Notifications: simplify the flow by removing persistence (#2924)
* Notifications: simplify the flow by removing persistence. * Bump of minSdk to `24` (Android 7). * Add migration to remove `notification.bin` file
This commit is contained in:
committed by
GitHub
parent
edc589b494
commit
801f0b955d
@@ -20,6 +20,9 @@ object NotificationConfig {
|
||||
// TODO EAx Implement and set to true at some point
|
||||
const val SUPPORT_MARK_AS_READ_ACTION = false
|
||||
|
||||
// TODO EAx Implement and set to true at some point
|
||||
const val SUPPORT_JOIN_DECLINE_INVITE = false
|
||||
|
||||
// TODO EAx Implement and set to true at some point
|
||||
const val SUPPORT_QUICK_REPLY_ACTION = false
|
||||
}
|
||||
|
||||
3
changelog.d/2924.misc
Normal file
3
changelog.d/2924.misc
Normal file
@@ -0,0 +1,3 @@
|
||||
Simplify notifications by removing the custom persistence layer.
|
||||
|
||||
Bump minSdk to 24 (Android 7).
|
||||
@@ -72,15 +72,10 @@ class CallForegroundService : Service() {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
|
||||
@@ -112,7 +112,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
trigger = JoinedRoom.Trigger.Invite,
|
||||
)
|
||||
.onSuccess {
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
.map { roomId }
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
roomId
|
||||
}.runCatchingUpdatingState(declinedAction)
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package io.element.android.features.login.impl.oidc.webview
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
@@ -25,7 +23,6 @@ import android.webkit.WebViewClient
|
||||
class OidcWebViewClient(
|
||||
private val eventListener: WebViewEventListener,
|
||||
) : WebViewClient() {
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
return shouldOverrideUrl(request.url.toString())
|
||||
}
|
||||
@@ -36,7 +33,6 @@ class OidcWebViewClient(
|
||||
}
|
||||
|
||||
private fun shouldOverrideUrl(url: String): Boolean {
|
||||
// Timber.d("shouldOverrideUrl: $url")
|
||||
return eventListener.shouldOverrideUrlLoading(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.sessionStorage.implMemory)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.features.migration.impl.migrations
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Remove notifications.bin file, used to store notification data locally.
|
||||
*/
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class AppMigration04 @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppMigration {
|
||||
companion object {
|
||||
internal const val NOTIFICATION_FILE_NAME = "notifications.bin"
|
||||
}
|
||||
override val order: Int = 4
|
||||
|
||||
override suspend fun migrate() {
|
||||
runCatching { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.features.migration.impl.migrations
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AppMigration04Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
// Create fake temporary file at the path to be deleted
|
||||
val file = context.getDatabasePath(AppMigration04.NOTIFICATION_FILE_NAME)
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
assertThat(file.exists()).isTrue()
|
||||
|
||||
val migration = AppMigration04(context)
|
||||
|
||||
migration.migrate()
|
||||
|
||||
// Check that the file has been deleted
|
||||
assertThat(file.exists()).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,9 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -31,9 +29,9 @@ interface SystemNotificationsEnabledProvider {
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultSystemNotificationsEnabledProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
) : SystemNotificationsEnabledProvider {
|
||||
override fun notificationsEnabled(): Boolean {
|
||||
return NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
return notificationManager.areNotificationsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +58,16 @@ class AndroidMediaPreProcessorTest {
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
// Computing thumbnailFile is failing with Robolectric
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 114_867,
|
||||
thumbnailInfo = null,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
blurhash = "K13]7q%zWC00R4of%\$baad"
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
@@ -88,7 +87,6 @@ class AndroidMediaPreProcessorTest {
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
// Computing thumbnailFile is failing with Robolectric
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
|
||||
@@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface NotificationDrawerManager {
|
||||
fun clearMembershipNotificationForSession(sessionId: SessionId)
|
||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean)
|
||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ dependencies {
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.troubleshoot.api)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
@@ -63,8 +64,8 @@ dependencies {
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coil.test)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object PushModule {
|
||||
@Provides
|
||||
fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat {
|
||||
return NotificationManagerCompat.from(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ActiveNotificationsProvider {
|
||||
fun getAllNotifications(): List<StatusBarNotification>
|
||||
fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
|
||||
fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification>
|
||||
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
|
||||
fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
|
||||
fun getSummaryNotification(sessionId: SessionId): StatusBarNotification?
|
||||
fun count(sessionId: SessionId): Int
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultActiveNotificationsProvider @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications
|
||||
}
|
||||
|
||||
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications.filter { it.groupKey == sessionId.value }
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
|
||||
}
|
||||
|
||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||
val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
||||
}
|
||||
|
||||
override fun count(sessionId: SessionId): Int {
|
||||
return getNotificationsForSession(sessionId).size
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,13 @@
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
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 androidx.annotation.VisibleForTesting
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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.RoomId
|
||||
@@ -33,45 +32,36 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.currentSessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
/**
|
||||
* The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and
|
||||
* The NotificationDrawerManager receives notification events as they arrive (from event stream or fcm) and
|
||||
* organise them in order to display them in the notification drawer.
|
||||
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
|
||||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultNotificationDrawerManager @Inject constructor(
|
||||
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
private val notificationEventPersistence: NotificationEventPersistence,
|
||||
private val filteredEventDetector: FilteredEventDetector,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val buildMeta: BuildMeta,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||
) : NotificationDrawerManager {
|
||||
private var appNavigationStateObserver: Job? = null
|
||||
|
||||
/**
|
||||
* 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 firstThrottler = FirstThrottler(200)
|
||||
|
||||
// TODO EAx add a setting per user for this
|
||||
private var useCompleteNotificationFormat = true
|
||||
|
||||
@@ -84,7 +74,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||
}
|
||||
|
||||
// For test only
|
||||
fun destroy() {
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun destroy() {
|
||||
appNavigationStateObserver?.cancel()
|
||||
}
|
||||
|
||||
@@ -105,7 +96,6 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||
clearMessagesForRoom(
|
||||
sessionId = navigationState.parentSpace.parentSession.sessionId,
|
||||
roomId = navigationState.roomId,
|
||||
doRender = true,
|
||||
)
|
||||
}
|
||||
is NavigationState.Thread -> {
|
||||
@@ -119,95 +109,71 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||
currentAppNavigationState = navigationState
|
||||
}
|
||||
|
||||
private fun createInitialNotificationState(): NotificationState {
|
||||
val queuedEvents = notificationEventPersistence.loadEvents(factory = { rawEvents ->
|
||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
})
|
||||
val renderedEvents = queuedEvents.rawEvents().map { ProcessedEvent(ProcessedEvent.Type.KEEP, it) }.toMutableList()
|
||||
return NotificationState(queuedEvents, renderedEvents)
|
||||
}
|
||||
|
||||
private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): $notifiableEvent")
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
||||
}
|
||||
|
||||
if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
|
||||
Timber.tag(loggerTag.value).d("onNotifiableEventReceived(): ignore the event")
|
||||
return
|
||||
}
|
||||
|
||||
add(notifiableEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called as soon as a new event is ready to be displayed.
|
||||
* The notification corresponding to this event will not be displayed until
|
||||
* #refreshNotificationDrawer() is called.
|
||||
* Should be called as soon as a new event is ready to be displayed, filtering out notifications that shouldn't be displayed.
|
||||
* Events might be grouped and there might not be one notification per event!
|
||||
*/
|
||||
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
updateEvents(doRender = true) {
|
||||
it.onNotifiableEventReceived(notifiableEvent)
|
||||
suspend fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
if (notifiableEvent.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value)) {
|
||||
return
|
||||
}
|
||||
renderEvents(listOf(notifiableEvent))
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all known events and refresh the notification drawer.
|
||||
* Clear all known message events for a [sessionId].
|
||||
*/
|
||||
fun clearAllMessagesEvents(sessionId: SessionId, doRender: Boolean) {
|
||||
updateEvents(doRender = doRender) {
|
||||
it.clearMessagesForSession(sessionId)
|
||||
}
|
||||
fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications related to the session and refresh the notification drawer.
|
||||
* Clear all notifications related to the session.
|
||||
*/
|
||||
fun clearAllEvents(sessionId: SessionId) {
|
||||
updateEvents(doRender = true) {
|
||||
it.clearAllForSession(sessionId)
|
||||
}
|
||||
activeNotificationsProvider.getNotificationsForSession(sessionId)
|
||||
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the application is currently opened and showing timeline for the given roomId.
|
||||
* Should be called when the application is currently opened and showing timeline for the given [roomId].
|
||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||
* Can also be called when a notification for this room is dismissed by the user.
|
||||
*/
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) {
|
||||
updateEvents(doRender = doRender) {
|
||||
it.clearMessagesForRoom(sessionId, roomId)
|
||||
}
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
|
||||
updateEvents(doRender = true) {
|
||||
it.clearMembershipNotificationForSession(sessionId)
|
||||
}
|
||||
activeNotificationsProvider.getMembershipNotificationForSession(sessionId)
|
||||
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear invitation notification for the provided room.
|
||||
*/
|
||||
override fun clearMembershipNotificationForRoom(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
doRender: Boolean,
|
||||
) {
|
||||
updateEvents(doRender = doRender) {
|
||||
it.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
}
|
||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
activeNotificationsProvider.getMembershipNotificationForRoom(sessionId, roomId)
|
||||
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the notifications for a single event.
|
||||
*/
|
||||
fun clearEvent(sessionId: SessionId, eventId: EventId, doRender: Boolean) {
|
||||
updateEvents(doRender = doRender) {
|
||||
it.clearEvent(sessionId, eventId)
|
||||
fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||
notificationManager.cancel(eventId.value, id)
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) {
|
||||
val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId)
|
||||
if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) {
|
||||
notificationManager.cancel(null, summaryNotification.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,69 +181,19 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||
* Should be called when the application is currently opened and showing timeline for the given threadId.
|
||||
* Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room.
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun onEnteringThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
|
||||
updateEvents(doRender = true) {
|
||||
it.clearMessagesForThread(sessionId, roomId, threadId)
|
||||
}
|
||||
// TODO maybe we'll have to embed more data in the tag to get a threadId
|
||||
// Do nothing for now
|
||||
}
|
||||
|
||||
private fun updateEvents(
|
||||
doRender: Boolean,
|
||||
action: (NotificationEventQueue) -> Unit,
|
||||
) {
|
||||
notificationState.updateQueuedEvents { queuedEvents, _ ->
|
||||
action(queuedEvents)
|
||||
}
|
||||
coroutineScope.refreshNotificationDrawer(doRender)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.refreshNotificationDrawer(doRender: Boolean) = launch {
|
||||
// Implement last throttler
|
||||
val canHandle = firstThrottler.canHandle()
|
||||
Timber.tag(loggerTag.value).v("refreshNotificationDrawer($doRender), delay: ${canHandle.waitMillis()} ms")
|
||||
withContext(dispatchers.io) {
|
||||
delay(canHandle.waitMillis())
|
||||
try {
|
||||
refreshNotificationDrawerBg(doRender)
|
||||
} 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.tag(loggerTag.value).w(throwable, "refreshNotificationDrawerBg failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshNotificationDrawerBg(doRender: Boolean) {
|
||||
Timber.tag(loggerTag.value).v("refreshNotificationDrawerBg($doRender)")
|
||||
val eventsToRender = notificationState.updateQueuedEvents { queuedEvents, renderedEvents ->
|
||||
notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also {
|
||||
queuedEvents.clearAndAdd(it.onlyKeptEvents())
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationState.hasAlreadyRendered(eventsToRender)) {
|
||||
Timber.tag(loggerTag.value).d("Skipping notification update due to event list not changing")
|
||||
} else {
|
||||
notificationState.clearAndAddRenderedEvents(eventsToRender)
|
||||
if (doRender) {
|
||||
renderEvents(eventsToRender)
|
||||
}
|
||||
persistEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistEvents() {
|
||||
notificationState.queuedEvents { queuedEvents ->
|
||||
notificationEventPersistence.persistEvents(queuedEvents)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
|
||||
private suspend fun renderEvents(eventsToRender: List<NotifiableEvent>) {
|
||||
// Group by sessionId
|
||||
val eventsForSessions = eventsToRender.groupBy {
|
||||
it.event.sessionId
|
||||
it.sessionId
|
||||
}
|
||||
|
||||
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
|
||||
for ((sessionId, notifiableEvents) in eventsForSessions) {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
|
||||
val imageLoader = imageLoaderHolder.get(client)
|
||||
val userFromCache = client.userProfile.value
|
||||
@@ -285,27 +201,29 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||
// We have an avatar and a display name, use it
|
||||
userFromCache
|
||||
} else {
|
||||
tryOrNull(
|
||||
onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
|
||||
operation = {
|
||||
client.getUserProfile().getOrNull()
|
||||
?.let {
|
||||
// displayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||
if (it.displayName.isNullOrEmpty()) {
|
||||
it.copy(displayName = sessionId.value)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
) ?: MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = sessionId.value,
|
||||
avatarUrl = null
|
||||
)
|
||||
client.getSafeUserProfile()
|
||||
}
|
||||
|
||||
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.getSafeUserProfile(): MatrixUser {
|
||||
return tryOrNull(
|
||||
onError = { 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
|
||||
private const val FILE_NAME = "notifications.bin"
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationEventPersistence @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : NotificationEventPersistence {
|
||||
private val file by lazy {
|
||||
deleteLegacyFileIfAny()
|
||||
context.getDatabasePath(FILE_NAME)
|
||||
}
|
||||
|
||||
private val encryptedFile by lazy {
|
||||
EncryptedFileFactory(context).create(file)
|
||||
}
|
||||
|
||||
override fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||
val rawEvents: ArrayList<NotifiableEvent>? = file
|
||||
.takeIf { it.exists() }
|
||||
?.let {
|
||||
try {
|
||||
encryptedFile.openFileInput().use { fis ->
|
||||
ObjectInputStream(fis).use { ois ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ois.readObject() as? ArrayList<NotifiableEvent>
|
||||
}
|
||||
}.also {
|
||||
Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info")
|
||||
null
|
||||
}
|
||||
}
|
||||
return factory(rawEvents.orEmpty())
|
||||
}
|
||||
|
||||
override fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
|
||||
// Always delete file before writing, or encryptedFile.openFileOutput() will throw
|
||||
file.safeDelete()
|
||||
if (queuedEvents.isEmpty()) return
|
||||
try {
|
||||
encryptedFile.openFileOutput().use { fos ->
|
||||
ObjectOutputStream(fos).use { oos ->
|
||||
oos.writeObject(queuedEvents.rawEvents())
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteLegacyFileIfAny() {
|
||||
tryOrNull {
|
||||
File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
class FilteredEventDetector @Inject constructor(
|
||||
// private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) {
|
||||
/**
|
||||
* Returns true if the given event should be ignored.
|
||||
* Used to skip notifications if a non expected message is received.
|
||||
*/
|
||||
fun shouldBeIgnored(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
|
||||
/* TODO EAx
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
val room = session.getRoom(notifiableEvent.roomId) ?: return false
|
||||
val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
|
||||
return timelineEvent.shouldBeIgnored()
|
||||
}
|
||||
|
||||
*/
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Whether the timeline event should be ignored.
|
||||
*/
|
||||
private fun TimelineEvent.shouldBeIgnored(): Boolean {
|
||||
if (root.isVoiceMessage()) {
|
||||
val audioEvent = root.asMessageAudioEvent()
|
||||
// if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
|
||||
return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
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.NotifiableEvent
|
||||
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.shouldIgnoreEventInRoom
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
|
||||
|
||||
private val loggerTag = LoggerTag("NotifiableEventProcessor", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
class NotifiableEventProcessor @Inject constructor(
|
||||
private val outdatedDetector: OutdatedEventDetector,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
) {
|
||||
fun process(
|
||||
queuedEvents: List<NotifiableEvent>,
|
||||
renderedEvents: ProcessedEvents,
|
||||
): ProcessedEvents {
|
||||
val appState = appNavigationStateService.appNavigationState.value
|
||||
val processedEvents = queuedEvents.map {
|
||||
val type = when (it) {
|
||||
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
|
||||
is NotifiableMessageEvent -> when {
|
||||
it.shouldIgnoreEventInRoom(appState) -> {
|
||||
ProcessedEvent.Type.REMOVE
|
||||
.also { Timber.tag(loggerTag.value).d("notification message removed due to currently viewing the same room or thread") }
|
||||
}
|
||||
outdatedDetector.isMessageOutdated(it) -> ProcessedEvent.Type.REMOVE
|
||||
.also { Timber.tag(loggerTag.value).d("notification message removed due to being read") }
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
is SimpleNotifiableEvent -> when (it.type) {
|
||||
EventType.REDACTION -> ProcessedEvent.Type.REMOVE
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
is FallbackNotifiableEvent -> when {
|
||||
it.shouldIgnoreEventInRoom(appState) -> {
|
||||
ProcessedEvent.Type.REMOVE
|
||||
.also { Timber.tag(loggerTag.value).d("notification fallback removed due to currently viewing the same room or thread") }
|
||||
}
|
||||
else -> ProcessedEvent.Type.KEEP
|
||||
}
|
||||
}
|
||||
ProcessedEvent(type, it)
|
||||
}
|
||||
|
||||
val removedEventsDiff = renderedEvents.filter { renderedEvent ->
|
||||
queuedEvents.none { it.eventId == renderedEvent.event.eventId }
|
||||
}.map { ProcessedEvent(ProcessedEvent.Type.REMOVE, it.event) }
|
||||
|
||||
return removedEventsDiff + processedEvents
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,17 @@ package io.element.android.libraries.push.impl.notifications
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -33,87 +39,69 @@ private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.Not
|
||||
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
|
||||
*/
|
||||
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
@Inject lateinit var appCoroutineScope: CoroutineScope
|
||||
@Inject lateinit var matrixClientProvider: MatrixClientProvider
|
||||
@Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory
|
||||
@Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
|
||||
@Inject lateinit var actionIds: NotificationActionIds
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent == null || context == null) return
|
||||
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
|
||||
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
||||
val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
|
||||
|
||||
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
|
||||
|
||||
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
||||
when (intent.action) {
|
||||
actionIds.smartReply ->
|
||||
handleSmartReply(intent, context)
|
||||
actionIds.dismissRoom -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissSummary ->
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId, doRender = false)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
|
||||
actionIds.dismissInvite -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissEvent -> if (eventId != null) {
|
||||
defaultNotificationDrawerManager.clearEvent(sessionId, eventId, doRender = false)
|
||||
defaultNotificationDrawerManager.clearEvent(sessionId, eventId)
|
||||
}
|
||||
actionIds.markRoomRead -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
handleMarkAsRead(sessionId, roomId)
|
||||
}
|
||||
actionIds.join -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleJoinRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.reject -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleRejectRoom(sessionId, roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
val room = session.getRoom(roomId)
|
||||
if (room != null) {
|
||||
session.coroutineScope.launch {
|
||||
tryOrNull {
|
||||
session.roomService().joinRoom(room.roomId)
|
||||
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Notification))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.joinRoom(roomId)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
session.coroutineScope.launch {
|
||||
tryOrNull { session.roomService().leaveRoom(roomId) }
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getRoom(roomId)?.leave()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
val room = session.getRoom(roomId)
|
||||
if (room != null) {
|
||||
session.coroutineScope.launch {
|
||||
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = false) }
|
||||
}
|
||||
}
|
||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
val isRenderReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isRenderReadReceiptsEnabled().first()
|
||||
val receiptType = if (isRenderReadReceiptsEnabled) {
|
||||
ReceiptType.READ
|
||||
} else {
|
||||
ReceiptType.READ_PRIVATE
|
||||
}
|
||||
|
||||
*/
|
||||
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import android.app.Notification
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import coil.ImageLoader
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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.factories.NotificationCreator
|
||||
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.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
interface NotificationDataFactory {
|
||||
suspend fun toNotifications(
|
||||
messages: List<NotifiableMessageEvent>,
|
||||
currentUser: MatrixUser,
|
||||
imageLoader: ImageLoader,
|
||||
): List<RoomNotification>
|
||||
|
||||
@JvmName("toNotificationInvites")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification>
|
||||
@JvmName("toNotificationSimpleEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
|
||||
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
|
||||
|
||||
fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): SummaryNotification
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationDataFactory @Inject constructor(
|
||||
private val notificationCreator: NotificationCreator,
|
||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator,
|
||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
) : NotificationDataFactory {
|
||||
override suspend fun toNotifications(
|
||||
messages: List<NotifiableMessageEvent>,
|
||||
currentUser: MatrixUser,
|
||||
imageLoader: ImageLoader,
|
||||
): List<RoomNotification> {
|
||||
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
|
||||
.groupBy { it.roomId }
|
||||
return messagesToDisplay.map { (roomId, events) ->
|
||||
val roomName = events.lastOrNull()?.roomName ?: roomId.value
|
||||
val isDirect = events.lastOrNull()?.roomIsDirect ?: false
|
||||
val notification = roomGroupMessageCreator.createRoomMessage(
|
||||
currentUser = currentUser,
|
||||
events = events,
|
||||
roomId = roomId,
|
||||
imageLoader = imageLoader,
|
||||
existingNotification = getExistingNotificationForMessages(currentUser.userId, roomId),
|
||||
)
|
||||
RoomNotification(
|
||||
notification = notification,
|
||||
roomId = roomId,
|
||||
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, isDirect),
|
||||
messageCount = events.size,
|
||||
latestTimestamp = events.maxOf { it.timestamp },
|
||||
shouldBing = events.any { it.noisy }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted
|
||||
|
||||
private fun getExistingNotificationForMessages(sessionId: SessionId, roomId: RoomId): Notification? {
|
||||
return activeNotificationsProvider.getMessageNotificationsForRoom(sessionId, roomId).firstOrNull()?.notification
|
||||
}
|
||||
|
||||
@JvmName("toNotificationInvites")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification> {
|
||||
return invites.map { event ->
|
||||
OneShotNotification(
|
||||
key = event.roomId.value,
|
||||
notification = notificationCreator.createRoomInvitationNotification(event),
|
||||
summaryLine = event.description,
|
||||
isNoisy = event.noisy,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("toNotificationSimpleEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification> {
|
||||
return simpleEvents.map { event ->
|
||||
OneShotNotification(
|
||||
key = event.eventId.value,
|
||||
notification = notificationCreator.createSimpleEventNotification(event),
|
||||
summaryLine = event.description,
|
||||
isNoisy = event.noisy,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||
return fallback.map { event ->
|
||||
OneShotNotification(
|
||||
key = event.eventId.value,
|
||||
notification = notificationCreator.createFallbackNotification(event),
|
||||
summaryLine = event.description.orEmpty(),
|
||||
isNoisy = false,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): SummaryNotification {
|
||||
return when {
|
||||
roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty() -> SummaryNotification.Removed
|
||||
else -> SummaryNotification.Update(
|
||||
summaryGroupMessageCreator.createSummaryNotification(
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
simpleNotifications = simpleNotifications,
|
||||
fallbackNotifications = fallbackNotifications,
|
||||
useCompleteNotificationFormat = useCompleteNotificationFormat
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return when (events.size) {
|
||||
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||
else -> {
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_compat_summary_line_for_room,
|
||||
events.size,
|
||||
roomName,
|
||||
events.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return if (roomIsDirect) {
|
||||
buildSpannedString {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(it)
|
||||
append(": ")
|
||||
}
|
||||
}
|
||||
append(event.description)
|
||||
}
|
||||
} else {
|
||||
buildSpannedString {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(roomName)
|
||||
append(": ")
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
append(it)
|
||||
append(" ")
|
||||
}
|
||||
}
|
||||
append(event.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class RoomNotification(
|
||||
val notification: Notification,
|
||||
val roomId: RoomId,
|
||||
val summaryLine: CharSequence,
|
||||
val messageCount: Int,
|
||||
val latestTimestamp: Long,
|
||||
val shouldBing: Boolean,
|
||||
) {
|
||||
fun isDataEqualTo(other: RoomNotification): Boolean {
|
||||
return notification == other.notification &&
|
||||
roomId == other.roomId &&
|
||||
summaryLine.toString() == other.summaryLine.toString() &&
|
||||
messageCount == other.messageCount &&
|
||||
latestTimestamp == other.latestTimestamp &&
|
||||
shouldBing == other.shouldBing
|
||||
}
|
||||
}
|
||||
|
||||
data class OneShotNotification(
|
||||
val notification: Notification,
|
||||
val key: String,
|
||||
val summaryLine: CharSequence,
|
||||
val isNoisy: Boolean,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
||||
sealed interface SummaryNotification {
|
||||
data object Removed : SummaryNotification
|
||||
data class Update(val notification: Notification) : SummaryNotification
|
||||
}
|
||||
@@ -22,16 +22,25 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationDisplayer @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
interface NotificationDisplayer {
|
||||
fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean
|
||||
fun cancelNotificationMessage(tag: String?, id: Int)
|
||||
fun displayDiagnosticNotification(notification: Notification): Boolean
|
||||
fun dismissDiagnosticNotification()
|
||||
}
|
||||
|
||||
fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean {
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationDisplayer @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat
|
||||
) : NotificationDisplayer {
|
||||
override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
Timber.w("Not allowed to notify.")
|
||||
return false
|
||||
@@ -40,20 +49,11 @@ class NotificationDisplayer @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun cancelNotificationMessage(tag: String?, id: Int) {
|
||||
override fun cancelNotificationMessage(tag: String?, id: Int) {
|
||||
notificationManager.cancel(tag, id)
|
||||
}
|
||||
|
||||
fun cancelAllNotifications() {
|
||||
// Keep this try catch (reported by GA)
|
||||
try {
|
||||
notificationManager.cancelAll()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## cancelAllNotifications() failed")
|
||||
}
|
||||
}
|
||||
|
||||
fun displayDiagnosticNotification(notification: Notification): Boolean {
|
||||
override fun displayDiagnosticNotification(notification: Notification): Boolean {
|
||||
return showNotificationMessage(
|
||||
tag = "DIAGNOSTIC",
|
||||
id = NOTIFICATION_ID_DIAGNOSTIC,
|
||||
@@ -61,33 +61,17 @@ class NotificationDisplayer @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissDiagnosticNotification() {
|
||||
override fun dismissDiagnosticNotification() {
|
||||
cancelNotificationMessage(
|
||||
tag = "DIAGNOSTIC",
|
||||
id = NOTIFICATION_ID_DIAGNOSTIC
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the foreground notification service.
|
||||
*/
|
||||
fun cancelNotificationForegroundService() {
|
||||
notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/* ==========================================================================================
|
||||
* IDs for notifications
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Identifier of the foreground notification used to keep the application alive
|
||||
* when it runs in background.
|
||||
* This notification, which is not removable by the end user, displays what
|
||||
* the application is doing while in background.
|
||||
*/
|
||||
private const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61
|
||||
|
||||
private const val NOTIFICATION_ID_DIAGNOSTIC = 888
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
||||
interface NotificationEventPersistence {
|
||||
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue
|
||||
fun persistEvents(queuedEvents: NotificationEventQueue)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import io.element.android.libraries.core.cache.CircularCache
|
||||
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.ThreadId
|
||||
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.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import timber.log.Timber
|
||||
|
||||
data class NotificationEventQueue(
|
||||
private val queue: MutableList<NotifiableEvent>,
|
||||
/**
|
||||
* An in memory FIFO cache of the seen events.
|
||||
* Acts as a notification debouncer to stop already dismissed push notifications from
|
||||
* displaying again when the /sync response is delayed.
|
||||
* TODO Should be per session, so the key must be Pair<SessionId, EventId>.
|
||||
*/
|
||||
private val seenEventIds: CircularCache<EventId>
|
||||
) {
|
||||
fun markRedacted(eventIds: List<EventId>) {
|
||||
eventIds.forEach { redactedId ->
|
||||
queue.replace(redactedId) {
|
||||
when (it) {
|
||||
is InviteNotifiableEvent -> it.copy(isRedacted = true)
|
||||
is NotifiableMessageEvent -> it.copy(isRedacted = true)
|
||||
is SimpleNotifiableEvent -> it.copy(isRedacted = true)
|
||||
is FallbackNotifiableEvent -> it.copy(isRedacted = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO EAx call this
|
||||
fun syncRoomEvents(roomsLeft: Collection<RoomId>, roomsJoined: Collection<RoomId>) {
|
||||
if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) {
|
||||
queue.removeAll {
|
||||
when (it) {
|
||||
is NotifiableMessageEvent -> roomsLeft.contains(it.roomId)
|
||||
is InviteNotifiableEvent -> roomsLeft.contains(it.roomId) || roomsJoined.contains(it.roomId)
|
||||
is SimpleNotifiableEvent -> false
|
||||
is FallbackNotifiableEvent -> roomsLeft.contains(it.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = queue.isEmpty()
|
||||
|
||||
fun clearAndAdd(events: List<NotifiableEvent>) {
|
||||
queue.clear()
|
||||
queue.addAll(events)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
queue.clear()
|
||||
}
|
||||
|
||||
fun add(notifiableEvent: NotifiableEvent) {
|
||||
val existing = findExistingById(notifiableEvent)
|
||||
val edited = findEdited(notifiableEvent)
|
||||
when {
|
||||
existing != null -> {
|
||||
if (existing.canBeReplaced) {
|
||||
// Use the event coming from the event stream as it may contains more info than
|
||||
// the fcm one (like type/content/clear text) (e.g when an encrypted message from
|
||||
// FCM should be update with clear text after a sync)
|
||||
// In this case the message has already been notified, and might have done some noise
|
||||
// So we want the notification to be updated even if it has already been displayed
|
||||
// Use setOnlyAlertOnce to ensure update notification does not interfere with sound
|
||||
// from first notify invocation as outlined in:
|
||||
// https://developer.android.com/training/notify-user/build-notification#Updating
|
||||
replace(replace = existing, with = notifiableEvent)
|
||||
} else {
|
||||
// keep the existing one, do not replace
|
||||
}
|
||||
}
|
||||
edited != null -> {
|
||||
// Replace the existing notification with the new content
|
||||
replace(replace = edited, with = notifiableEvent)
|
||||
}
|
||||
seenEventIds.contains(notifiableEvent.eventId) -> {
|
||||
// we've already seen the event, lets skip
|
||||
Timber.d("onNotifiableEventReceived(): skipping event, already seen")
|
||||
}
|
||||
else -> {
|
||||
seenEventIds.put(notifiableEvent.eventId)
|
||||
queue.add(notifiableEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findExistingById(notifiableEvent: NotifiableEvent): NotifiableEvent? {
|
||||
return queue.firstOrNull { it.sessionId == notifiableEvent.sessionId && it.eventId == notifiableEvent.eventId }
|
||||
}
|
||||
|
||||
private fun findEdited(notifiableEvent: NotifiableEvent): NotifiableEvent? {
|
||||
return notifiableEvent.editedEventId?.let { editedId ->
|
||||
queue.firstOrNull {
|
||||
it.eventId == editedId || it.editedEventId == editedId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replace(replace: NotifiableEvent, with: NotifiableEvent) {
|
||||
queue.remove(replace)
|
||||
queue.add(
|
||||
when (with) {
|
||||
is InviteNotifiableEvent -> with.copy(isUpdated = true)
|
||||
is NotifiableMessageEvent -> with.copy(isUpdated = true)
|
||||
is SimpleNotifiableEvent -> with.copy(isUpdated = true)
|
||||
is FallbackNotifiableEvent -> with.copy(isUpdated = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
val isFallback = queue.firstOrNull { it.sessionId == sessionId && it.eventId == eventId } is FallbackNotifiableEvent
|
||||
if (isFallback) {
|
||||
Timber.d("Removing all the fallbacks")
|
||||
queue.removeAll { it.sessionId == sessionId && it is FallbackNotifiableEvent }
|
||||
} else {
|
||||
queue.removeAll { it.sessionId == sessionId && it.eventId == eventId }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMembershipNotificationForSession(sessionId: SessionId) {
|
||||
Timber.d("clearMemberShipOfSession $sessionId")
|
||||
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
Timber.d("clearMemberShipOfRoom $sessionId, $roomId")
|
||||
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId }
|
||||
}
|
||||
|
||||
fun clearMessagesForSession(sessionId: SessionId) {
|
||||
Timber.d("clearMessagesForSession $sessionId")
|
||||
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
fun clearAllForSession(sessionId: SessionId) {
|
||||
Timber.d("clearAllForSession $sessionId")
|
||||
queue.removeAll { it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
Timber.d("clearMessageEventOfRoom $sessionId, $roomId")
|
||||
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }
|
||||
}
|
||||
|
||||
fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
|
||||
Timber.d("clearMessageEventOfThread $sessionId, $roomId, $threadId")
|
||||
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId }
|
||||
}
|
||||
|
||||
fun rawEvents(): List<NotifiableEvent> = queue
|
||||
}
|
||||
|
||||
private fun MutableList<NotifiableEvent>.replace(eventId: EventId, block: (NotifiableEvent) -> NotifiableEvent) {
|
||||
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
||||
if (indexToReplace == -1) {
|
||||
return
|
||||
}
|
||||
set(indexToReplace, block(get(indexToReplace)))
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import android.app.Notification
|
||||
import coil.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
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.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||
|
||||
class NotificationFactory @Inject constructor(
|
||||
private val notificationCreator: NotificationCreator,
|
||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||
) {
|
||||
suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
|
||||
currentUser: MatrixUser,
|
||||
imageLoader: ImageLoader,
|
||||
): List<RoomNotification> {
|
||||
return map { (roomId, events) ->
|
||||
when {
|
||||
events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId)
|
||||
else -> {
|
||||
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
|
||||
roomGroupMessageCreator.createRoomMessage(
|
||||
currentUser = currentUser,
|
||||
events = messageEvents,
|
||||
roomId = roomId,
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProcessedMessageEvents.hasNoEventsToDisplay() = isEmpty() || all {
|
||||
it.type == ProcessedEvent.Type.REMOVE || it.event.canNotBeDisplayed()
|
||||
}
|
||||
|
||||
private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted
|
||||
|
||||
@JvmName("toNotificationsInviteNotifiableEvent")
|
||||
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
|
||||
return map { (processed, event) ->
|
||||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationCreator.createRoomInvitationNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.roomId.value,
|
||||
summaryLine = event.description,
|
||||
isNoisy = event.noisy,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("toNotificationsSimpleNotifiableEvent")
|
||||
fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
|
||||
return map { (processed, event) ->
|
||||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationCreator.createSimpleEventNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.eventId.value,
|
||||
summaryLine = event.description,
|
||||
isNoisy = event.noisy,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun List<ProcessedEvent<FallbackNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
|
||||
return map { (processed, event) ->
|
||||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationCreator.createFallbackNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.eventId.value,
|
||||
summaryLine = event.description.orEmpty(),
|
||||
isNoisy = false,
|
||||
timestamp = event.timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): SummaryNotification {
|
||||
val roomMeta = roomNotifications.filterIsInstance<RoomNotification.Message>().map { it.meta }
|
||||
val invitationMeta = invitationNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
|
||||
val simpleMeta = simpleNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
|
||||
val fallbackMeta = fallbackNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
|
||||
return when {
|
||||
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
|
||||
else -> SummaryNotification.Update(
|
||||
summaryGroupMessageCreator.createSummaryNotification(
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomMeta,
|
||||
invitationNotifications = invitationMeta,
|
||||
simpleNotifications = simpleMeta,
|
||||
fallbackNotifications = fallbackMeta,
|
||||
useCompleteNotificationFormat = useCompleteNotificationFormat
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface RoomNotification {
|
||||
data class Removed(val roomId: RoomId) : RoomNotification
|
||||
data class Message(val notification: Notification, val meta: Meta) : RoomNotification {
|
||||
data class Meta(
|
||||
val roomId: RoomId,
|
||||
val summaryLine: CharSequence,
|
||||
val messageCount: Int,
|
||||
val latestTimestamp: Long,
|
||||
val shouldBing: Boolean
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface OneShotNotification {
|
||||
data class Removed(val key: String) : OneShotNotification
|
||||
data class Append(val notification: Notification, val meta: Meta) : OneShotNotification {
|
||||
data class Meta(
|
||||
val key: String,
|
||||
val summaryLine: CharSequence,
|
||||
val isNoisy: Boolean,
|
||||
val timestamp: Long,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface SummaryNotification {
|
||||
data object Removed : SummaryNotification
|
||||
data class Update(val notification: Notification) : SummaryNotification
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
@@ -33,20 +32,20 @@ private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.Notification
|
||||
class NotificationRenderer @Inject constructor(
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val notificationDisplayer: NotificationDisplayer,
|
||||
private val notificationFactory: NotificationFactory,
|
||||
private val notificationDataFactory: NotificationDataFactory,
|
||||
) {
|
||||
suspend fun render(
|
||||
currentUser: MatrixUser,
|
||||
useCompleteNotificationFormat: Boolean,
|
||||
eventsToProcess: List<ProcessedEvent<NotifiableEvent>>,
|
||||
eventsToProcess: List<NotifiableEvent>,
|
||||
imageLoader: ImageLoader,
|
||||
) {
|
||||
val groupedEvents = eventsToProcess.groupByType()
|
||||
with(notificationFactory) {
|
||||
val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser, imageLoader)
|
||||
val invitationNotifications = groupedEvents.invitationEvents.toNotifications()
|
||||
val simpleNotifications = groupedEvents.simpleEvents.toNotifications()
|
||||
val fallbackNotifications = groupedEvents.fallbackEvents.toNotifications()
|
||||
with(notificationDataFactory) {
|
||||
val roomNotifications = toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
|
||||
val invitationNotifications = toNotifications(groupedEvents.invitationEvents)
|
||||
val simpleNotifications = toNotifications(groupedEvents.simpleEvents)
|
||||
val fallbackNotifications = toNotifications(groupedEvents.fallbackEvents)
|
||||
val summaryNotification = createSummaryNotification(
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomNotifications,
|
||||
@@ -65,101 +64,43 @@ class NotificationRenderer @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
roomNotifications.forEach { wrapper ->
|
||||
when (wrapper) {
|
||||
is RoomNotification.Removed -> {
|
||||
Timber.tag(loggerTag.value).d("Removing room messages notification ${wrapper.roomId}")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating room messages notification ${wrapper.meta.roomId}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = wrapper.meta.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invitationNotifications.forEach { wrapper ->
|
||||
when (wrapper) {
|
||||
is OneShotNotification.Removed -> {
|
||||
Timber.tag(loggerTag.value).d("Removing invitation notification ${wrapper.key}")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating invitation notification ${wrapper.meta.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = wrapper.meta.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simpleNotifications.forEach { wrapper ->
|
||||
when (wrapper) {
|
||||
is OneShotNotification.Removed -> {
|
||||
Timber.tag(loggerTag.value).d("Removing simple notification ${wrapper.key}")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating simple notification ${wrapper.meta.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = wrapper.meta.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fallbackNotifications.forEach { wrapper ->
|
||||
when (wrapper) {
|
||||
is OneShotNotification.Removed -> {
|
||||
Timber.tag(loggerTag.value).d("Removing fallback notification ${wrapper.key}")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = wrapper.key,
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating fallback notification ${wrapper.meta.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = wrapper.meta.key,
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = wrapper.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
val removedFallback = fallbackNotifications.filterIsInstance<OneShotNotification.Removed>()
|
||||
val appendFallback = fallbackNotifications.filterIsInstance<OneShotNotification.Append>()
|
||||
if (appendFallback.isEmpty() && removedFallback.isNotEmpty()) {
|
||||
Timber.tag(loggerTag.value).d("Removing global fallback notification")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId)
|
||||
roomNotifications.forEach { notificationData ->
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
} else if (appendFallback.isNotEmpty()) {
|
||||
}
|
||||
|
||||
invitationNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
simpleNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show only the first fallback notification
|
||||
if (fallbackNotifications.isNotEmpty()) {
|
||||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = appendFallback.first().notification
|
||||
notification = fallbackNotifications.first().notification
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,39 +115,30 @@ class NotificationRenderer @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAllNotifications() {
|
||||
notificationDisplayer.cancelAllNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents {
|
||||
val roomIdToEventMap: MutableMap<RoomId, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap()
|
||||
val simpleEvents: MutableList<ProcessedEvent<SimpleNotifiableEvent>> = ArrayList()
|
||||
val invitationEvents: MutableList<ProcessedEvent<InviteNotifiableEvent>> = ArrayList()
|
||||
val fallbackEvents: MutableList<ProcessedEvent<FallbackNotifiableEvent>> = ArrayList()
|
||||
forEach {
|
||||
when (val event = it.event) {
|
||||
is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType())
|
||||
is NotifiableMessageEvent -> {
|
||||
val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() }
|
||||
roomEvents.add(it.castedToEventType())
|
||||
}
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType())
|
||||
is FallbackNotifiableEvent -> {
|
||||
fallbackEvents.add(it.castedToEventType())
|
||||
}
|
||||
private fun List<NotifiableEvent>.groupByType(): GroupedNotificationEvents {
|
||||
val roomEvents: MutableList<NotifiableMessageEvent> = mutableListOf()
|
||||
val simpleEvents: MutableList<SimpleNotifiableEvent> = mutableListOf()
|
||||
val invitationEvents: MutableList<InviteNotifiableEvent> = mutableListOf()
|
||||
val fallbackEvents: MutableList<FallbackNotifiableEvent> = mutableListOf()
|
||||
forEach { event ->
|
||||
when (event) {
|
||||
is InviteNotifiableEvent -> invitationEvents.add(event.castedToEventType())
|
||||
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
|
||||
is FallbackNotifiableEvent -> fallbackEvents.add(event.castedToEventType())
|
||||
}
|
||||
}
|
||||
return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents, fallbackEvents)
|
||||
return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : NotifiableEvent> ProcessedEvent<NotifiableEvent>.castedToEventType(): ProcessedEvent<T> = this as ProcessedEvent<T>
|
||||
private fun <T : NotifiableEvent> NotifiableEvent.castedToEventType(): T = this as T
|
||||
|
||||
data class GroupedNotificationEvents(
|
||||
val roomEvents: Map<RoomId, List<ProcessedEvent<NotifiableMessageEvent>>>,
|
||||
val simpleEvents: List<ProcessedEvent<SimpleNotifiableEvent>>,
|
||||
val invitationEvents: List<ProcessedEvent<InviteNotifiableEvent>>,
|
||||
val fallbackEvents: List<ProcessedEvent<FallbackNotifiableEvent>>,
|
||||
val roomEvents: List<NotifiableMessageEvent>,
|
||||
val simpleEvents: List<SimpleNotifiableEvent>,
|
||||
val invitationEvents: List<InviteNotifiableEvent>,
|
||||
val fallbackEvents: List<FallbackNotifiableEvent>,
|
||||
)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
||||
class NotificationState(
|
||||
/**
|
||||
* The notifiable events queued for rendering or currently rendered.
|
||||
*
|
||||
* This is our source of truth for notifications, any changes to this list will be rendered as notifications.
|
||||
* When events are removed the previously rendered notifications will be cancelled.
|
||||
* When adding or updating, the notifications will be notified.
|
||||
*
|
||||
* Events are unique by their properties, we should be careful not to insert multiple events with the same event-id.
|
||||
*/
|
||||
private val queuedEvents: NotificationEventQueue,
|
||||
/**
|
||||
* The last known rendered notifiable events.
|
||||
* We keep track of them in order to know which events have been removed from the eventList
|
||||
* allowing us to cancel any notifications previous displayed by now removed events
|
||||
*/
|
||||
private val renderedEvents: MutableList<ProcessedEvent<NotifiableEvent>>,
|
||||
) {
|
||||
fun <T> updateQueuedEvents(
|
||||
action: (NotificationEventQueue, List<ProcessedEvent<NotifiableEvent>>) -> T
|
||||
): T {
|
||||
return synchronized(queuedEvents) {
|
||||
action(queuedEvents, renderedEvents)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAndAddRenderedEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
|
||||
renderedEvents.clear()
|
||||
renderedEvents.addAll(eventsToRender)
|
||||
}
|
||||
|
||||
fun hasAlreadyRendered(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) = renderedEvents == eventsToRender
|
||||
|
||||
fun queuedEvents(block: (NotificationEventQueue) -> Unit) {
|
||||
synchronized(queuedEvents) {
|
||||
block(queuedEvents)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutdatedEventDetector @Inject constructor(
|
||||
// / private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) {
|
||||
/**
|
||||
* Returns true if the given event is outdated.
|
||||
* Used to clean up notifications if a displayed message has been read on an
|
||||
* other device.
|
||||
*/
|
||||
fun isMessageOutdated(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
|
||||
/* TODO EAx
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
val eventID = notifiableEvent.eventId
|
||||
val roomID = notifiableEvent.roomId
|
||||
val room = session.getRoom(roomID) ?: return false
|
||||
return room.readService().isEventRead(eventID)
|
||||
}
|
||||
|
||||
*/
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
data class ProcessedEvent<T>(
|
||||
val type: Type,
|
||||
val event: T
|
||||
) {
|
||||
enum class Type {
|
||||
KEEP,
|
||||
REMOVE
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> List<ProcessedEvent<T>>.onlyKeptEvents() = mapNotNull { processedEvent ->
|
||||
processedEvent.event.takeIf { processedEvent.type == ProcessedEvent.Type.KEEP }
|
||||
}
|
||||
@@ -16,48 +16,46 @@
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import coil.ImageLoader
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomGroupMessageCreator @Inject constructor(
|
||||
private val bitmapLoader: NotificationBitmapLoader,
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationCreator: NotificationCreator
|
||||
) {
|
||||
interface RoomGroupMessageCreator {
|
||||
suspend fun createRoomMessage(
|
||||
currentUser: MatrixUser,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
roomId: RoomId,
|
||||
imageLoader: ImageLoader,
|
||||
): RoomNotification.Message {
|
||||
existingNotification: Notification?,
|
||||
): Notification
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRoomGroupMessageCreator @Inject constructor(
|
||||
private val bitmapLoader: NotificationBitmapLoader,
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationCreator: NotificationCreator,
|
||||
) : RoomGroupMessageCreator {
|
||||
override suspend fun createRoomMessage(
|
||||
currentUser: MatrixUser,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
roomId: RoomId,
|
||||
imageLoader: ImageLoader,
|
||||
existingNotification: Notification?,
|
||||
): Notification {
|
||||
val lastKnownRoomEvent = events.last()
|
||||
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)"
|
||||
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
||||
val style = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName(currentUser.displayName?.annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl, imageLoader))
|
||||
.setKey(lastKnownRoomEvent.sessionId.value)
|
||||
.build()
|
||||
).also {
|
||||
it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
|
||||
it.isGroupConversation = roomIsGroup
|
||||
it.addMessagesFromEvents(events, imageLoader)
|
||||
}
|
||||
|
||||
val tickerText = if (roomIsGroup) {
|
||||
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderDisambiguatedDisplayName, events.last().description)
|
||||
@@ -69,112 +67,28 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
|
||||
val lastMessageTimestamp = events.last().timestamp
|
||||
val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||
val messageCount = events.size - smartReplyErrors.size
|
||||
val meta = RoomNotification.Message.Meta(
|
||||
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup),
|
||||
messageCount = messageCount,
|
||||
latestTimestamp = lastMessageTimestamp,
|
||||
roomId = roomId,
|
||||
shouldBing = events.any { it.noisy }
|
||||
)
|
||||
return RoomNotification.Message(
|
||||
notificationCreator.createMessagesListNotification(
|
||||
style,
|
||||
return notificationCreator.createMessagesListNotification(
|
||||
RoomEventGroupInfo(
|
||||
sessionId = currentUser.userId,
|
||||
roomId = roomId,
|
||||
roomDisplayName = roomName,
|
||||
isDirect = !roomIsGroup,
|
||||
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
|
||||
shouldBing = meta.shouldBing,
|
||||
shouldBing = events.any { it.noisy },
|
||||
customSound = events.last().soundName,
|
||||
isUpdated = events.last().isUpdated,
|
||||
),
|
||||
threadId = lastKnownRoomEvent.threadId,
|
||||
largeIcon = largeBitmap,
|
||||
lastMessageTimestamp,
|
||||
tickerText
|
||||
),
|
||||
meta
|
||||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
tickerText = tickerText,
|
||||
currentUser = currentUser,
|
||||
existingNotification = existingNotification,
|
||||
imageLoader = imageLoader,
|
||||
events = events,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(
|
||||
events: List<NotifiableMessageEvent>,
|
||||
imageLoader: ImageLoader,
|
||||
) {
|
||||
events.forEach { event ->
|
||||
val senderPerson = if (event.outGoingMessage) {
|
||||
null
|
||||
} else {
|
||||
Person.Builder()
|
||||
.setName(event.senderDisambiguatedDisplayName?.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
|
||||
.setKey(event.senderId.value)
|
||||
.build()
|
||||
}
|
||||
when {
|
||||
event.isSmartReplyError() -> addMessage(
|
||||
stringProvider.getString(R.string.notification_inline_reply_failed),
|
||||
event.timestamp,
|
||||
senderPerson
|
||||
)
|
||||
else -> {
|
||||
val message = NotificationCompat.MessagingStyle.Message(
|
||||
event.body?.annotateForDebug(71),
|
||||
event.timestamp,
|
||||
senderPerson
|
||||
).also { message ->
|
||||
event.imageUri?.let {
|
||||
message.setData("image/", it)
|
||||
}
|
||||
}
|
||||
addMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return when (events.size) {
|
||||
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||
else -> {
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_compat_summary_line_for_room,
|
||||
events.size,
|
||||
roomName,
|
||||
events.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return if (roomIsDirect) {
|
||||
buildSpannedString {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(it)
|
||||
append(": ")
|
||||
}
|
||||
}
|
||||
append(event.description)
|
||||
}
|
||||
} else {
|
||||
buildSpannedString {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(roomName)
|
||||
append(": ")
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
append(it)
|
||||
append(" ")
|
||||
}
|
||||
}
|
||||
append(event.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getRoomBitmap(
|
||||
events: List<NotifiableMessageEvent>,
|
||||
imageLoader: ImageLoader,
|
||||
@@ -184,5 +98,3 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||
?.let { bitmapLoader.getRoomBitmap(it, imageLoader) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed
|
||||
|
||||
@@ -18,6 +18,8 @@ package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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
|
||||
@@ -25,30 +27,37 @@ import io.element.android.libraries.push.impl.notifications.factories.Notificati
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
interface SummaryGroupMessageCreator {
|
||||
fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): Notification
|
||||
}
|
||||
|
||||
/**
|
||||
* ======== Build summary notification =========
|
||||
* On Android 7.0 (API level 24) and higher, the system automatically builds a summary for
|
||||
* your group using snippets of text from each notification. The user can expand this
|
||||
* notification to see each separate notification.
|
||||
* To support older versions, which cannot show a nested group of notifications,
|
||||
* you must create an extra notification that acts as the summary.
|
||||
* This appears as the only notification and the system hides all the others.
|
||||
* So this summary should include a snippet from all the other notifications,
|
||||
* which the user can tap to open your app.
|
||||
* The behavior of the group summary may vary on some device types such as wearables.
|
||||
* To ensure the best experience on all devices and versions, always include a group summary when you create a group
|
||||
* https://developer.android.com/training/notify-user/group
|
||||
*/
|
||||
class SummaryGroupMessageCreator @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultSummaryGroupMessageCreator @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationCreator: NotificationCreator,
|
||||
) {
|
||||
fun createSummaryNotification(
|
||||
) : SummaryGroupMessageCreator {
|
||||
override fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification.Message.Meta>,
|
||||
invitationNotifications: List<OneShotNotification.Append.Meta>,
|
||||
simpleNotifications: List<OneShotNotification.Append.Meta>,
|
||||
fallbackNotifications: List<OneShotNotification.Append.Meta>,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): Notification {
|
||||
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
|
||||
|
||||
@@ -36,10 +36,9 @@ import javax.inject.Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class NotificationChannels @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val stringProvider: StringProvider,
|
||||
) {
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
init {
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
||||
@@ -22,26 +22,80 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.MessagingStyle
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import coil.ImageLoader
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.NotificationConfig
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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.NotificationBitmapLoader
|
||||
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
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||
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.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationCreator @Inject constructor(
|
||||
interface NotificationCreator {
|
||||
/**
|
||||
* Create a notification for a Room.
|
||||
*/
|
||||
suspend fun createMessagesListNotification(
|
||||
roomInfo: RoomEventGroupInfo,
|
||||
threadId: ThreadId?,
|
||||
largeIcon: Bitmap?,
|
||||
lastMessageTimestamp: Long,
|
||||
tickerText: String,
|
||||
currentUser: MatrixUser,
|
||||
existingNotification: Notification?,
|
||||
imageLoader: ImageLoader,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
): Notification
|
||||
|
||||
fun createRoomInvitationNotification(
|
||||
inviteNotifiableEvent: InviteNotifiableEvent
|
||||
): Notification
|
||||
|
||||
fun createSimpleEventNotification(
|
||||
simpleNotifiableEvent: SimpleNotifiableEvent,
|
||||
): Notification
|
||||
|
||||
fun createFallbackNotification(
|
||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
||||
): Notification
|
||||
|
||||
/**
|
||||
* Create the summary notification.
|
||||
*/
|
||||
fun createSummaryListNotification(
|
||||
currentUser: MatrixUser,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
lastMessageTimestamp: Long
|
||||
): Notification
|
||||
|
||||
fun createDiagnosticNotification(): Notification
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationCreator @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val stringProvider: StringProvider,
|
||||
@@ -49,17 +103,23 @@ class NotificationCreator @Inject constructor(
|
||||
private val pendingIntentFactory: PendingIntentFactory,
|
||||
private val markAsReadActionFactory: MarkAsReadActionFactory,
|
||||
private val quickReplyActionFactory: QuickReplyActionFactory,
|
||||
) {
|
||||
private val bitmapLoader: NotificationBitmapLoader,
|
||||
private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
|
||||
private val rejectInvitationActionFactory: RejectInvitationActionFactory
|
||||
) : NotificationCreator {
|
||||
/**
|
||||
* Create a notification for a Room.
|
||||
*/
|
||||
fun createMessagesListNotification(
|
||||
messageStyle: NotificationCompat.MessagingStyle,
|
||||
override suspend fun createMessagesListNotification(
|
||||
roomInfo: RoomEventGroupInfo,
|
||||
threadId: ThreadId?,
|
||||
largeIcon: Bitmap?,
|
||||
lastMessageTimestamp: Long,
|
||||
tickerText: String
|
||||
tickerText: String,
|
||||
currentUser: MatrixUser,
|
||||
existingNotification: Notification?,
|
||||
imageLoader: ImageLoader,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
// Build the pending intent for when the notification is clicked
|
||||
@@ -71,17 +131,39 @@ class NotificationCreator @Inject constructor(
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
val builder = if (existingNotification != null) {
|
||||
NotificationCompat.Builder(context, existingNotification)
|
||||
} else {
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
.setShortcutId(roomInfo.roomId.value)
|
||||
// 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)
|
||||
// In order to avoid notification making sound twice (due to the summary notification)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
// Remove notification after opening it or using an action
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
val messagingStyle = existingNotification?.let {
|
||||
MessagingStyle.extractMessagingStyleFromNotification(it)
|
||||
} ?: messagingStyleFromCurrentUser(roomInfo.sessionId, currentUser, imageLoader, roomInfo.roomDisplayName, !roomInfo.isDirect)
|
||||
|
||||
messagingStyle.addMessagesFromEvents(events, imageLoader)
|
||||
|
||||
return builder
|
||||
.setNumber(events.size)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
.setWhen(lastMessageTimestamp)
|
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices.
|
||||
.setStyle(messageStyle)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
// ID of the corresponding shortcut, for conversation features under API 30+
|
||||
.setShortcutId(roomInfo.roomId.value)
|
||||
.setStyle(messagingStyle)
|
||||
// Not needed anymore?
|
||||
// Title for API < 16 devices.
|
||||
.setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1))
|
||||
// Content for API < 16 devices.
|
||||
@@ -90,15 +172,10 @@ class NotificationCreator @Inject constructor(
|
||||
.setSubText(
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_new_messages_for_room,
|
||||
messageStyle.messages.size,
|
||||
messageStyle.messages.size
|
||||
messagingStyle.messages.size,
|
||||
messagingStyle.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
|
||||
.setGroup(roomInfo.sessionId.value)
|
||||
// In order to avoid notification making sound twice (due to the summary notification)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
// Set primary color (important for Wear 2.0 Notifications).
|
||||
.setColor(accentColor)
|
||||
@@ -118,7 +195,8 @@ class NotificationCreator @Inject constructor(
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
|
||||
// Clear existing actions since we might be updating an existing notification
|
||||
clearActions()
|
||||
// Add actions and notification intents
|
||||
// Mark room as read
|
||||
addAction(markAsReadActionFactory.create(roomInfo))
|
||||
@@ -134,11 +212,11 @@ class NotificationCreator @Inject constructor(
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
}
|
||||
.setTicker(tickerText.annotateForDebug(4))
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createRoomInvitationNotification(
|
||||
override fun createRoomInvitationNotification(
|
||||
inviteNotifiableEvent: InviteNotifiableEvent
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
@@ -152,10 +230,11 @@ class NotificationCreator @Inject constructor(
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setColor(accentColor)
|
||||
// TODO removed for now, will be added back later
|
||||
// .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
// .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.apply {
|
||||
if (NotificationConfig.SUPPORT_JOIN_DECLINE_INVITE) {
|
||||
addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
}
|
||||
// Build the pending intent for when the notification is clicked
|
||||
setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(inviteNotifiableEvent.sessionId, inviteNotifiableEvent.roomId))
|
||||
|
||||
@@ -182,7 +261,7 @@ class NotificationCreator @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createSimpleEventNotification(
|
||||
override fun createSimpleEventNotification(
|
||||
simpleNotifiableEvent: SimpleNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
@@ -212,12 +291,11 @@ class NotificationCreator @Inject constructor(
|
||||
} else {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createFallbackNotification(
|
||||
override fun createFallbackNotification(
|
||||
fallbackNotifiableEvent: FallbackNotifiableEvent,
|
||||
): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
@@ -244,17 +322,14 @@ class NotificationCreator @Inject constructor(
|
||||
fallbackNotifiableEvent.eventId
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
setAutoCancel(true)
|
||||
}
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the summary notification.
|
||||
*/
|
||||
fun createSummaryListNotification(
|
||||
override fun createSummaryListNotification(
|
||||
currentUser: MatrixUser,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
@@ -298,7 +373,7 @@ class NotificationCreator @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
fun createDiagnosticNotification(): Notification {
|
||||
override fun createDiagnosticNotification(): Notification {
|
||||
val intent = pendingIntentFactory.createTestPendingIntent()
|
||||
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
|
||||
.setContentTitle(buildMeta.applicationName)
|
||||
@@ -314,6 +389,61 @@ class NotificationCreator @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun MessagingStyle.addMessagesFromEvents(
|
||||
events: List<NotifiableMessageEvent>,
|
||||
imageLoader: ImageLoader,
|
||||
) {
|
||||
events.forEach { event ->
|
||||
val senderPerson = if (event.outGoingMessage) {
|
||||
null
|
||||
} else {
|
||||
Person.Builder()
|
||||
.setName(event.senderDisambiguatedDisplayName?.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
|
||||
.setKey(event.senderId.value)
|
||||
.build()
|
||||
}
|
||||
when {
|
||||
event.isSmartReplyError() -> addMessage(
|
||||
stringProvider.getString(R.string.notification_inline_reply_failed),
|
||||
event.timestamp,
|
||||
senderPerson
|
||||
)
|
||||
else -> {
|
||||
val message = MessagingStyle.Message(
|
||||
event.body?.annotateForDebug(71),
|
||||
event.timestamp,
|
||||
senderPerson
|
||||
).also { message ->
|
||||
event.imageUri?.let {
|
||||
message.setData("image/", it)
|
||||
}
|
||||
}
|
||||
addMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun messagingStyleFromCurrentUser(
|
||||
sessionId: SessionId,
|
||||
user: MatrixUser,
|
||||
imageLoader: ImageLoader,
|
||||
roomName: String,
|
||||
roomIsGroup: Boolean
|
||||
): MessagingStyle {
|
||||
return MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName(user.displayName?.annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
|
||||
.setKey(sessionId.value)
|
||||
.build()
|
||||
).also {
|
||||
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||
it.isGroupConversation = roomIsGroup
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBitmap(@DrawableRes drawableRes: Int): Bitmap? {
|
||||
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null
|
||||
val canvas = Canvas()
|
||||
@@ -324,3 +454,5 @@ class NotificationCreator @Inject constructor(
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
||||
fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed
|
||||
|
||||
@@ -46,21 +46,20 @@ class QuickReplyActionFactory @Inject constructor(
|
||||
if (!NotificationConfig.SUPPORT_QUICK_REPLY_ACTION) return null
|
||||
val sessionId = roomInfo.sessionId
|
||||
val roomId = roomInfo.roomId
|
||||
return buildQuickReplyIntent(sessionId, roomId, threadId)?.let { replyPendingIntent ->
|
||||
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
.setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply))
|
||||
.build()
|
||||
val replyPendingIntent = buildQuickReplyIntent(sessionId, roomId, threadId)
|
||||
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
.setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply))
|
||||
.build()
|
||||
|
||||
NotificationCompat.Action.Builder(
|
||||
R.drawable.vector_notification_quick_reply,
|
||||
stringProvider.getString(R.string.notification_room_action_quick_reply),
|
||||
replyPendingIntent
|
||||
)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(false)
|
||||
.build()
|
||||
}
|
||||
return NotificationCompat.Action.Builder(
|
||||
R.drawable.vector_notification_quick_reply,
|
||||
stringProvider.getString(R.string.notification_room_action_quick_reply),
|
||||
replyPendingIntent
|
||||
)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -74,30 +73,26 @@ class QuickReplyActionFactory @Inject constructor(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
threadId: ThreadId?,
|
||||
): PendingIntent? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||
intent.action = actionIds.smartReply
|
||||
intent.data = createIgnoredUri("quickReply/$sessionId/$roomId" + threadId?.let { "/$it" }.orEmpty())
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
|
||||
threadId?.let {
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value)
|
||||
}
|
||||
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
clock.epochMillis().toInt(),
|
||||
intent,
|
||||
// PendingIntents attached to actions with remote inputs must be mutable
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
} else {
|
||||
null
|
||||
): PendingIntent {
|
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
|
||||
intent.action = actionIds.smartReply
|
||||
intent.data = createIgnoredUri("quickReply/$sessionId/$roomId" + threadId?.let { "/$it" }.orEmpty())
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
|
||||
threadId?.let {
|
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value)
|
||||
}
|
||||
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
clock.epochMillis().toInt(),
|
||||
intent,
|
||||
// PendingIntents attached to actions with remote inputs must be mutable
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
interface OnNotifiableEventReceived {
|
||||
@@ -29,8 +31,11 @@ interface OnNotifiableEventReceived {
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOnNotifiableEventReceived @Inject constructor(
|
||||
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : OnNotifiableEventReceived {
|
||||
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
coroutineScope.launch {
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
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_2
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultActiveNotificationsProviderTest {
|
||||
@Test
|
||||
fun `getAllNotifications with no active notifications returns empty list`() {
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
|
||||
|
||||
val emptyNotifications = activeNotificationsProvider.getAllNotifications()
|
||||
assertThat(emptyNotifications).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAllNotifications with active notifications returns all`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
val result = activeNotificationsProvider.getAllNotifications()
|
||||
assertThat(result).hasSize(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNotificationsForSession returns only notifications for that session id`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
assertThat(activeNotificationsProvider.getNotificationsForSession(A_SESSION_ID)).hasSize(1)
|
||||
assertThat(activeNotificationsProvider.getNotificationsForSession(A_SESSION_ID_2)).hasSize(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),
|
||||
groupId = A_SESSION_ID_2.value,
|
||||
tag = A_ROOM_ID.value,
|
||||
),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
assertThat(activeNotificationsProvider.getMembershipNotificationForSession(A_SESSION_ID)).isEmpty()
|
||||
assertThat(activeNotificationsProvider.getMembershipNotificationForSession(A_SESSION_ID_2)).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||
groupId = A_SESSION_ID.value,
|
||||
tag = A_ROOM_ID.value
|
||||
),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value, tag = A_ROOM_ID.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID_2),
|
||||
groupId = A_SESSION_ID_2.value,
|
||||
tag = A_ROOM_ID.value
|
||||
),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value, tag = A_ROOM_ID.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),
|
||||
groupId = A_SESSION_ID_2.value,
|
||||
tag = A_ROOM_ID.value
|
||||
),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
assertThat(activeNotificationsProvider.getMessageNotificationsForRoom(A_SESSION_ID, A_ROOM_ID)).hasSize(1)
|
||||
assertThat(activeNotificationsProvider.getMessageNotificationsForRoom(A_SESSION_ID_2, A_ROOM_ID_2)).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||
groupId = A_SESSION_ID.value,
|
||||
tag = A_ROOM_ID.value
|
||||
),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value, tag = A_ROOM_ID.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),
|
||||
groupId = A_SESSION_ID_2.value,
|
||||
tag = A_ROOM_ID_2.value
|
||||
),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value, tag = A_ROOM_ID.value),
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),
|
||||
groupId = A_SESSION_ID_2.value,
|
||||
tag = A_ROOM_ID_2.value
|
||||
),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
assertThat(activeNotificationsProvider.getMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)).isEmpty()
|
||||
assertThat(activeNotificationsProvider.getMembershipNotificationForRoom(A_SESSION_ID_2, A_ROOM_ID_2)).hasSize(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSummaryNotification returns only the summary notification for that session id if it exists`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID)).isNotNull()
|
||||
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull()
|
||||
}
|
||||
|
||||
private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk<StatusBarNotification> {
|
||||
every { this@mockk.id } returns id
|
||||
every { this@mockk.groupKey } returns groupId
|
||||
every { this@mockk.tag } returns tag
|
||||
}
|
||||
|
||||
private fun createActiveNotificationsProvider(
|
||||
activeNotifications: List<StatusBarNotification> = emptyList(),
|
||||
): DefaultActiveNotificationsProvider {
|
||||
val notificationManager = mockk<NotificationManagerCompat> {
|
||||
every { this@mockk.activeNotifications } returns activeNotifications
|
||||
}
|
||||
return DefaultActiveNotificationsProvider(
|
||||
notificationManager = notificationManager,
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,25 +16,36 @@
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
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.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator
|
||||
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.FakeSummaryGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.element.android.services.appnavstate.test.aNavigationState
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -58,16 +69,29 @@ class DefaultNotificationDrawerManagerTest {
|
||||
@Test
|
||||
fun `cover all APIs`() = runTest {
|
||||
// For now just call all the API. Later, add more valuable tests.
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false)
|
||||
val matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data")
|
||||
val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator(
|
||||
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, existingNotification, ->
|
||||
assertThat(user).isEqualTo(matrixUser)
|
||||
assertThat(roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(existingNotification).isNull()
|
||||
Notification()
|
||||
}
|
||||
)
|
||||
val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||
roomGroupMessageCreator = mockRoomGroupMessageCreator,
|
||||
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
||||
)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForSession(A_SESSION_ID)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
// Add the same Event again (will be ignored)
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
@@ -103,33 +127,93 @@ class DefaultNotificationDrawerManagerTest {
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when MatrixClient has no cached user name a fallback one is used to render the notification`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(userDisplayName = null)
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
|
||||
val messageCreator = FakeRoomGroupMessageCreator()
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
roomGroupMessageCreator = messageCreator,
|
||||
)
|
||||
// Gets a display name from MatrixClient.getUserProfile
|
||||
matrixClient.givenGetProfileResult(A_SESSION_ID, Result.success(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice")))
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
|
||||
// Uses the user id as a fallback value since display name is blank
|
||||
matrixClient.givenGetProfileResult(A_SESSION_ID, Result.success(aMatrixUser(id = A_SESSION_ID.value, displayName = "")))
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
|
||||
// Uses the user id as a fallback value since the result fails
|
||||
matrixClient.givenGetProfileResult(A_SESSION_ID, Result.failure(IllegalStateException("Failed to get profile")))
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
|
||||
messageCreator.createRoomMessageResult.assertions()
|
||||
.isCalledExactly(3)
|
||||
.withSequence(
|
||||
listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice")), any(), any(), any(), any()),
|
||||
listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value)), any(), any(), any(), any()),
|
||||
listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value, avatarUrl = AN_AVATAR_URL)), any(), any(), any(), any()),
|
||||
)
|
||||
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearSummaryNotificationIfNeeded will run after clearing all other notifications`() = runTest {
|
||||
val notificationManager = mockk<NotificationManagerCompat> {
|
||||
every { cancel(any(), any()) } returns Unit
|
||||
}
|
||||
val summaryId = NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)
|
||||
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||
mutableListOf(
|
||||
mockk {
|
||||
every { id } returns summaryId
|
||||
}
|
||||
)
|
||||
)
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||
notificationManager = notificationManager,
|
||||
activeNotificationsProvider = activeNotificationsProvider,
|
||||
)
|
||||
|
||||
// Ask to clear all existing message notifications. Since only the summary notification is left, it should be cleared
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID)
|
||||
|
||||
// Verify we asked to cancel the notification with summaryId
|
||||
verify { notificationManager.cancel(null, summaryId) }
|
||||
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultNotificationDrawerManager(
|
||||
notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(RuntimeEnvironment.getApplication()),
|
||||
appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(),
|
||||
initialData: List<NotifiableEvent> = emptyList()
|
||||
roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(),
|
||||
summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(),
|
||||
activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
): DefaultNotificationDrawerManager {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return DefaultNotificationDrawerManager(
|
||||
notifiableEventProcessor = NotifiableEventProcessor(
|
||||
outdatedDetector = OutdatedEventDetector(),
|
||||
appNavigationStateService = appNavigationStateService
|
||||
),
|
||||
notificationManager = notificationManager,
|
||||
notificationRenderer = NotificationRenderer(
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
notificationDisplayer = NotificationDisplayer(context),
|
||||
notificationFactory = NotificationFactory(
|
||||
notificationCreator = MockkNotificationCreator().instance,
|
||||
roomGroupMessageCreator = MockkRoomGroupMessageCreator().instance,
|
||||
summaryGroupMessageCreator = MockkSummaryGroupMessageCreator().instance,
|
||||
)
|
||||
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)),
|
||||
notificationDataFactory = DefaultNotificationDataFactory(
|
||||
notificationCreator = FakeNotificationCreator(),
|
||||
roomGroupMessageCreator = roomGroupMessageCreator,
|
||||
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
||||
activeNotificationsProvider = activeNotificationsProvider,
|
||||
stringProvider = FakeStringProvider(),
|
||||
),
|
||||
),
|
||||
notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData),
|
||||
filteredEventDetector = FilteredEventDetector(),
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
appNavigationStateService = appNavigationStateService,
|
||||
coroutineScope = this,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
buildMeta = aBuildMeta(),
|
||||
matrixClientProvider = FakeMatrixClientProvider(),
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
activeNotificationsProvider = activeNotificationsProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.cache.CircularCache
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultNotificationEventPersistenceTest {
|
||||
@Test
|
||||
fun `loadEvents should return empty NotificationEventQueue`() {
|
||||
val sut = createDefaultNotificationEventPersistence()
|
||||
val result = sut.loadEvents(
|
||||
factory = { rawEvents ->
|
||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
}
|
||||
)
|
||||
assertThat(result.isEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `after persisting NotificationEventQueue, loadEvents should return non-empty NotificationEventQueue`() {
|
||||
val sut = createDefaultNotificationEventPersistence()
|
||||
val notificationEventQueue = NotificationEventQueue(mutableListOf(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
// First persist an empty queue
|
||||
sut.persistEvents(notificationEventQueue)
|
||||
// Add an event
|
||||
notificationEventQueue.add(aSimpleNotifiableEvent())
|
||||
// Persist
|
||||
// Note: is cannot work because AndroidKeyStore is not available. But we check that the code does
|
||||
// not crash.
|
||||
sut.persistEvents(notificationEventQueue)
|
||||
sut.loadEvents(
|
||||
factory = { rawEvents ->
|
||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
}
|
||||
)
|
||||
// assertThat(result.isEmpty()).isFalse()
|
||||
}
|
||||
|
||||
private fun createDefaultNotificationEventPersistence(): DefaultNotificationEventPersistence {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return DefaultNotificationEventPersistence(context)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
@@ -42,7 +42,7 @@ private const val A_USER_AVATAR_1 = "mxc://userAvatar1"
|
||||
private const val A_USER_AVATAR_2 = "mxc://userAvatar2"
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class RoomGroupMessageCreatorTest {
|
||||
class DefaultRoomGroupMessageCreatorTest {
|
||||
@Test
|
||||
fun `test createRoomMessage with one Event`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
@@ -56,19 +56,12 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.number).isEqualTo(1)
|
||||
@Suppress("DEPRECATION")
|
||||
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW)
|
||||
assertThat(result.`when`).isEqualTo(A_TIMESTAMP)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@@ -85,19 +78,10 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = true,
|
||||
)
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@@ -115,7 +99,6 @@ class RoomGroupMessageCreatorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
@Test
|
||||
fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest {
|
||||
`test createRoomMessage with room avatar and sender avatar`(
|
||||
@@ -138,7 +121,6 @@ class RoomGroupMessageCreatorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
private fun `test createRoomMessage with room avatar and sender avatar`(
|
||||
api: Int,
|
||||
expectedCoilRequests: List<Any>,
|
||||
@@ -160,20 +142,10 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(fakeImageLoader.getCoilRequests()).isEqualTo(expectedCoilRequests)
|
||||
assertThat(result.number).isEqualTo(1)
|
||||
assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,19 +160,10 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: 2 messages",
|
||||
messageCount = 2,
|
||||
latestTimestamp = A_TIMESTAMP + 10,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.number).isEqualTo(2)
|
||||
assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@@ -218,19 +181,9 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 0,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.actions).isNull()
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@@ -247,19 +200,10 @@ class RoomGroupMessageCreatorTest {
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
existingNotification = null,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "sender-name: message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.number).isEqualTo(1)
|
||||
assertThat(result.`when`).isEqualTo(A_TIMESTAMP)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
@@ -268,12 +212,13 @@ fun createRoomGroupMessageCreator(
|
||||
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
|
||||
): RoomGroupMessageCreator {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
return RoomGroupMessageCreator(
|
||||
notificationCreator = createNotificationCreator(),
|
||||
bitmapLoader = NotificationBitmapLoader(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
sdkIntProvider = sdkIntProvider,
|
||||
),
|
||||
val bitmapLoader = NotificationBitmapLoader(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
sdkIntProvider = sdkIntProvider,
|
||||
)
|
||||
return DefaultRoomGroupMessageCreator(
|
||||
notificationCreator = createNotificationCreator(bitmapLoader = bitmapLoader),
|
||||
bitmapLoader = bitmapLoader,
|
||||
stringProvider = AndroidStringProvider(context.resources)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.fake.FakeNotificationCreator
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.nonNull
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultSummaryGroupMessageCreatorTest {
|
||||
@Test
|
||||
fun `process notifications with complete format`() = runTest {
|
||||
val notificationCreator = FakeNotificationCreator()
|
||||
val summaryCreator = DefaultSummaryGroupMessageCreator(
|
||||
stringProvider = FakeStringProvider(),
|
||||
notificationCreator = notificationCreator,
|
||||
)
|
||||
|
||||
val result = summaryCreator.createSummaryNotification(
|
||||
currentUser = aMatrixUser(),
|
||||
roomNotifications = listOf(
|
||||
RoomNotification(
|
||||
notification = Notification(),
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_FAKE_TIMESTAMP + 10,
|
||||
shouldBing = true,
|
||||
)
|
||||
),
|
||||
invitationNotifications = emptyList(),
|
||||
simpleNotifications = emptyList(),
|
||||
fallbackNotifications = emptyList(),
|
||||
useCompleteNotificationFormat = true,
|
||||
)
|
||||
|
||||
notificationCreator.createSummaryListNotificationResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(any(), nonNull(), any(), any(), any())
|
||||
|
||||
// Set from the events included
|
||||
@Suppress("DEPRECATION")
|
||||
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process notifications without complete format`() = runTest {
|
||||
val notificationCreator = FakeNotificationCreator()
|
||||
val summaryCreator = DefaultSummaryGroupMessageCreator(
|
||||
stringProvider = FakeStringProvider(),
|
||||
notificationCreator = notificationCreator,
|
||||
)
|
||||
|
||||
val result = summaryCreator.createSummaryNotification(
|
||||
currentUser = aMatrixUser(),
|
||||
roomNotifications = listOf(
|
||||
RoomNotification(
|
||||
notification = Notification(),
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_FAKE_TIMESTAMP + 10,
|
||||
shouldBing = true,
|
||||
)
|
||||
),
|
||||
invitationNotifications = emptyList(),
|
||||
simpleNotifications = emptyList(),
|
||||
fallbackNotifications = emptyList(),
|
||||
useCompleteNotificationFormat = false,
|
||||
)
|
||||
|
||||
notificationCreator.createSummaryListNotificationResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(any(), value(null), any(), any(), any())
|
||||
|
||||
// Set from the events included
|
||||
@Suppress("DEPRECATION")
|
||||
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
||||
class InMemoryNotificationEventPersistence(
|
||||
initialData: List<NotifiableEvent> = emptyList()
|
||||
) : NotificationEventPersistence {
|
||||
private var data: List<NotifiableEvent> = initialData
|
||||
|
||||
override fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||
return factory(data)
|
||||
}
|
||||
|
||||
override fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
data = queuedEvents.rawEvents()
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
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_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkOutdatedEventDetector
|
||||
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 io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.element.android.services.appnavstate.test.aNavigationState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Test
|
||||
|
||||
private val NOT_VIEWING_A_ROOM = aNavigationState()
|
||||
private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
|
||||
private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
|
||||
|
||||
class NotifiableEventProcessorTest {
|
||||
private val mockkOutdatedDetector = MockkOutdatedEventDetector()
|
||||
|
||||
@Test
|
||||
fun `given simple events when processing then keep simple events`() {
|
||||
val events = listOf(
|
||||
aSimpleNotifiableEvent(eventId = AN_EVENT_ID),
|
||||
aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2)
|
||||
)
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.KEEP to events[0],
|
||||
ProcessedEvent.Type.KEEP to events[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given redacted simple event when processing then remove redaction event`() {
|
||||
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.REMOVE to events[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given invites are not auto accepted when processing then keep invitation events`() {
|
||||
val events = listOf(
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID),
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID_2)
|
||||
)
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.KEEP to events[0],
|
||||
ProcessedEvent.Type.KEEP to events[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given out of date message event when processing then removes message event`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||
mockkOutdatedDetector.givenEventIsOutOfDate(events[0])
|
||||
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.REMOVE to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given in date message event when processing then keep message event`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||
mockkOutdatedDetector.givenEventIsInDate(events[0])
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.KEEP to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
|
||||
events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) }
|
||||
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.REMOVE to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
||||
events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) }
|
||||
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.REMOVE to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
|
||||
mockkOutdatedDetector.givenEventIsInDate(events[0])
|
||||
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.KEEP to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
|
||||
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
|
||||
mockkOutdatedDetector.givenEventIsInDate(events[0])
|
||||
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = emptyList())
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.KEEP to events[0],
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given events are different to rendered events when processing then removes difference`() {
|
||||
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))
|
||||
val renderedEvents = listOf<ProcessedEvent<NotifiableEvent>>(
|
||||
ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]),
|
||||
ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2))
|
||||
)
|
||||
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
|
||||
|
||||
val result = eventProcessor.process(events, renderedEvents = renderedEvents)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOfProcessedEvents(
|
||||
ProcessedEvent.Type.REMOVE to renderedEvents[1].event,
|
||||
ProcessedEvent.Type.KEEP to renderedEvents[0].event
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun listOfProcessedEvents(vararg event: Pair<ProcessedEvent.Type, NotifiableEvent>) = event.map {
|
||||
ProcessedEvent(it.first, it.second)
|
||||
}
|
||||
|
||||
private fun createProcessor(
|
||||
isInForeground: Boolean = false,
|
||||
navigationState: NavigationState
|
||||
): NotifiableEventProcessor {
|
||||
return NotifiableEventProcessor(
|
||||
outdatedDetector = mockkOutdatedDetector.instance,
|
||||
appNavigationStateService = FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
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
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
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.FakeSummaryGroupMessageCreator
|
||||
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 io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
private val MY_AVATAR_URL: String? = null
|
||||
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
|
||||
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationDataFactoryTest {
|
||||
private val notificationCreator = FakeNotificationCreator()
|
||||
private val fakeRoomGroupMessageCreator = FakeRoomGroupMessageCreator()
|
||||
private val fakeSummaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
||||
private val activeNotificationsProvider = FakeActiveNotificationsProvider()
|
||||
|
||||
private val notificationDataFactory = DefaultNotificationDataFactory(
|
||||
notificationCreator = notificationCreator,
|
||||
roomGroupMessageCreator = fakeRoomGroupMessageCreator,
|
||||
summaryGroupMessageCreator = fakeSummaryGroupMessageCreator,
|
||||
activeNotificationsProvider = activeNotificationsProvider,
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given a room invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) {
|
||||
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT)
|
||||
val roomInvitation = listOf(AN_INVITATION_EVENT)
|
||||
|
||||
val result = toNotifications(roomInvitation)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification(
|
||||
notification = expectedNotification,
|
||||
key = A_ROOM_ID.value,
|
||||
summaryLine = AN_INVITATION_EVENT.description,
|
||||
isNoisy = AN_INVITATION_EVENT.noisy,
|
||||
timestamp = AN_INVITATION_EVENT.timestamp
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) {
|
||||
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT)
|
||||
val roomInvitation = listOf(A_SIMPLE_EVENT)
|
||||
|
||||
val result = toNotifications(roomInvitation)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification(
|
||||
notification = expectedNotification,
|
||||
key = AN_EVENT_ID.value,
|
||||
summaryLine = A_SIMPLE_EVENT.description,
|
||||
isNoisy = A_SIMPLE_EVENT.noisy,
|
||||
timestamp = AN_INVITATION_EVENT.timestamp
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationDataFactory) {
|
||||
val events = listOf(A_MESSAGE_EVENT)
|
||||
val expectedNotification = RoomNotification(
|
||||
notification = fakeRoomGroupMessageCreator.createRoomMessage(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
events,
|
||||
A_ROOM_ID,
|
||||
FakeImageLoader().getImageLoader(),
|
||||
null,
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = events.size,
|
||||
latestTimestamp = events.maxOf { it.timestamp },
|
||||
shouldBing = events.any { it.noisy }
|
||||
)
|
||||
val roomWithMessage = listOf(A_MESSAGE_EVENT)
|
||||
|
||||
val fakeImageLoader = FakeImageLoader()
|
||||
val result = toNotifications(
|
||||
messages = roomWithMessage,
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result.size).isEqualTo(1)
|
||||
assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue()
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
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 fakeImageLoader = FakeImageLoader()
|
||||
val result = toNotifications(
|
||||
messages = redactedRoom,
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result).isEmpty()
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(
|
||||
notificationDataFactory
|
||||
) {
|
||||
val roomWithRedactedMessage = listOf(
|
||||
A_MESSAGE_EVENT.copy(isRedacted = true),
|
||||
A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")),
|
||||
)
|
||||
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
|
||||
val expectedNotification = RoomNotification(
|
||||
notification = fakeRoomGroupMessageCreator.createRoomMessage(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
withRedactedRemoved,
|
||||
A_ROOM_ID,
|
||||
FakeImageLoader().getImageLoader(),
|
||||
null,
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = withRedactedRemoved.size,
|
||||
latestTimestamp = withRedactedRemoved.maxOf { it.timestamp },
|
||||
shouldBing = withRedactedRemoved.any { it.noisy }
|
||||
)
|
||||
|
||||
val fakeImageLoader = FakeImageLoader()
|
||||
val result = toNotifications(
|
||||
messages = roomWithRedactedMessage,
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result.size).isEqualTo(1)
|
||||
assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue()
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
|
||||
runTest {
|
||||
receiver.block()
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.cache.CircularCache
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.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 io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationEventQueueTest {
|
||||
private val seenIdsCache = CircularCache.create<EventId>(5)
|
||||
|
||||
@Test
|
||||
fun `given events when redacting some then marks matching event ids as redacted`() {
|
||||
val queue = givenQueue(
|
||||
listOf(
|
||||
aSimpleNotifiableEvent(eventId = EventId("\$redacted-id-1")),
|
||||
aNotifiableMessageEvent(eventId = EventId("\$redacted-id-2")),
|
||||
anInviteNotifiableEvent(eventId = EventId("\$redacted-id-3")),
|
||||
aSimpleNotifiableEvent(eventId = EventId("\$kept-id")),
|
||||
)
|
||||
)
|
||||
|
||||
queue.markRedacted(listOf(EventId("\$redacted-id-1"), EventId("\$redacted-id-2"), EventId("\$redacted-id-3")))
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(
|
||||
listOf(
|
||||
aSimpleNotifiableEvent(eventId = EventId("\$redacted-id-1"), isRedacted = true),
|
||||
aNotifiableMessageEvent(eventId = EventId("\$redacted-id-2"), isRedacted = true),
|
||||
anInviteNotifiableEvent(eventId = EventId("\$redacted-id-3"), isRedacted = true),
|
||||
aSimpleNotifiableEvent(eventId = EventId("\$kept-id"), isRedacted = false),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given invite event when leaving invited room and syncing then removes event`() {
|
||||
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
|
||||
val roomsLeft = listOf(A_ROOM_ID)
|
||||
|
||||
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
|
||||
|
||||
assertThat(queue.rawEvents()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given invite event when joining invited room and syncing then removes event`() {
|
||||
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
|
||||
val joinedRooms = listOf(A_ROOM_ID)
|
||||
|
||||
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = joinedRooms)
|
||||
|
||||
assertThat(queue.rawEvents()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given message event when leaving message room and syncing then removes event`() {
|
||||
val queue = givenQueue(listOf(aNotifiableMessageEvent(roomId = A_ROOM_ID)))
|
||||
val roomsLeft = listOf(A_ROOM_ID)
|
||||
|
||||
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
|
||||
|
||||
assertThat(queue.rawEvents()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given events when syncing without rooms left or joined ids then does not change the events`() {
|
||||
val queue = givenQueue(
|
||||
listOf(
|
||||
aNotifiableMessageEvent(roomId = A_ROOM_ID),
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||
)
|
||||
)
|
||||
|
||||
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = emptyList())
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(
|
||||
listOf(
|
||||
aNotifiableMessageEvent(roomId = A_ROOM_ID),
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given events then is not empty`() {
|
||||
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||
|
||||
assertThat(queue.isEmpty()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no events then is empty`() {
|
||||
val queue = givenQueue(emptyList())
|
||||
|
||||
assertThat(queue.isEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given events when clearing and adding then removes previous events and adds only new events`() {
|
||||
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||
|
||||
queue.clearAndAdd(listOf(anInviteNotifiableEvent()))
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(anInviteNotifiableEvent()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when clearing then is empty`() {
|
||||
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
|
||||
|
||||
queue.clear()
|
||||
|
||||
assertThat(queue.rawEvents()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no events when adding then adds event`() {
|
||||
val queue = givenQueue(listOf())
|
||||
|
||||
queue.add(aSimpleNotifiableEvent())
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(aSimpleNotifiableEvent()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no events when adding already seen event then ignores event`() {
|
||||
val queue = givenQueue(listOf())
|
||||
val notifiableEvent = aSimpleNotifiableEvent()
|
||||
seenIdsCache.put(notifiableEvent.eventId)
|
||||
|
||||
queue.add(notifiableEvent)
|
||||
|
||||
assertThat(queue.rawEvents()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given replaceable event when adding event with same id then updates existing event`() {
|
||||
val replaceableEvent = aSimpleNotifiableEvent(canBeReplaced = true)
|
||||
val updatedEvent = replaceableEvent.copy(title = "updated title", isUpdated = true)
|
||||
val queue = givenQueue(listOf(replaceableEvent))
|
||||
|
||||
queue.add(updatedEvent)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given non replaceable event when adding event with same id then ignores event`() {
|
||||
val nonReplaceableEvent = aSimpleNotifiableEvent(canBeReplaced = false)
|
||||
val updatedEvent = nonReplaceableEvent.copy(title = "updated title")
|
||||
val queue = givenQueue(listOf(nonReplaceableEvent))
|
||||
|
||||
queue.add(updatedEvent)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(nonReplaceableEvent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given event when adding new event with edited event id matching the existing event id then updates existing event`() {
|
||||
val editedEvent = aSimpleNotifiableEvent(eventId = EventId("\$id-to-edit"))
|
||||
val updatedEvent = editedEvent.copy(eventId = EventId("\$1"), editedEventId = EventId("\$id-to-edit"), title = "updated title", isUpdated = true)
|
||||
val queue = givenQueue(listOf(editedEvent))
|
||||
|
||||
queue.add(updatedEvent)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given event when adding new event with edited event id matching the existing event edited id then updates existing event`() {
|
||||
val editedEvent = aSimpleNotifiableEvent(eventId = EventId("\$0"), editedEventId = EventId("\$id-to-edit"))
|
||||
val updatedEvent = editedEvent.copy(eventId = EventId("\$1"), editedEventId = EventId("\$id-to-edit"), title = "updated title", isUpdated = true)
|
||||
val queue = givenQueue(listOf(editedEvent))
|
||||
|
||||
queue.add(updatedEvent)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when clearing membership notification then removes invite events with matching room id`() {
|
||||
val queue = givenQueue(
|
||||
listOf(
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID),
|
||||
aNotifiableMessageEvent(roomId = A_ROOM_ID)
|
||||
)
|
||||
)
|
||||
|
||||
queue.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(aNotifiableMessageEvent(roomId = A_ROOM_ID)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when clearing messages for room then removes message events with matching room id`() {
|
||||
val queue = givenQueue(
|
||||
listOf(
|
||||
anInviteNotifiableEvent(roomId = A_ROOM_ID),
|
||||
aNotifiableMessageEvent(roomId = A_ROOM_ID)
|
||||
)
|
||||
)
|
||||
|
||||
queue.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID)
|
||||
|
||||
assertThat(queue.rawEvents()).isEqualTo(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
|
||||
}
|
||||
|
||||
private fun givenQueue(events: List<NotifiableEvent>) = NotificationEventQueue(events.toMutableList(), seenEventIds = seenIdsCache)
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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
|
||||
|
||||
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
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator
|
||||
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
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
private val MY_AVATAR_URL: String? = null
|
||||
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
|
||||
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationFactoryTest {
|
||||
private val mockkNotificationCreator = MockkNotificationCreator()
|
||||
private val mockkRoomGroupMessageCreator = MockkRoomGroupMessageCreator()
|
||||
private val mockkSummaryGroupMessageCreator = MockkSummaryGroupMessageCreator()
|
||||
|
||||
private val notificationFactory = NotificationFactory(
|
||||
notificationCreator = mockkNotificationCreator.instance,
|
||||
roomGroupMessageCreator = mockkRoomGroupMessageCreator.instance,
|
||||
summaryGroupMessageCreator = mockkSummaryGroupMessageCreator.instance
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||
val expectedNotification = mockkNotificationCreator.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT)
|
||||
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT))
|
||||
|
||||
val result = roomInvitation.toNotifications()
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification.Append(
|
||||
notification = expectedNotification,
|
||||
meta = OneShotNotification.Append.Meta(
|
||||
key = A_ROOM_ID.value,
|
||||
summaryLine = AN_INVITATION_EVENT.description,
|
||||
isNoisy = AN_INVITATION_EVENT.noisy,
|
||||
timestamp = AN_INVITATION_EVENT.timestamp
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) {
|
||||
val missingEventRoomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, AN_INVITATION_EVENT))
|
||||
|
||||
val result = missingEventRoomInvitation.toNotifications()
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification.Removed(
|
||||
key = A_ROOM_ID.value
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||
val expectedNotification = mockkNotificationCreator.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
|
||||
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT))
|
||||
|
||||
val result = roomInvitation.toNotifications()
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification.Append(
|
||||
notification = expectedNotification,
|
||||
meta = OneShotNotification.Append.Meta(
|
||||
key = AN_EVENT_ID.value,
|
||||
summaryLine = A_SIMPLE_EVENT.description,
|
||||
isNoisy = A_SIMPLE_EVENT.noisy,
|
||||
timestamp = AN_INVITATION_EVENT.timestamp
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) {
|
||||
val missingEventRoomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_SIMPLE_EVENT))
|
||||
|
||||
val result = missingEventRoomInvitation.toNotifications()
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
OneShotNotification.Removed(
|
||||
key = AN_EVENT_ID.value
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
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 = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor(
|
||||
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 fakeImageLoader = FakeImageLoader()
|
||||
val result = roomWithMessage.toNotifications(
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(listOf(expectedNotification))
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) {
|
||||
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
|
||||
val emptyRoom = mapOf(A_ROOM_ID to events)
|
||||
|
||||
val fakeImageLoader = FakeImageLoader()
|
||||
val result = emptyRoom.toNotifications(
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
RoomNotification.Removed(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
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 fakeImageLoader = FakeImageLoader()
|
||||
val result = redactedRoom.toNotifications(
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
RoomNotification.Removed(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(
|
||||
notificationFactory
|
||||
) {
|
||||
val roomWithRedactedMessage = mapOf(
|
||||
A_ROOM_ID to listOf(
|
||||
ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)),
|
||||
ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
|
||||
)
|
||||
)
|
||||
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
|
||||
val expectedNotification = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor(
|
||||
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
withRedactedRemoved,
|
||||
A_ROOM_ID,
|
||||
)
|
||||
|
||||
val fakeImageLoader = FakeImageLoader()
|
||||
val result = roomWithRedactedMessage.toNotifications(
|
||||
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
|
||||
imageLoader = fakeImageLoader.getImageLoader(),
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(listOf(expectedNotification))
|
||||
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
|
||||
runTest {
|
||||
receiver.block()
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,24 @@
|
||||
|
||||
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
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationFactory
|
||||
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.FakeSummaryGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
|
||||
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 io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.mockk.mockk
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -35,201 +43,81 @@ private const val MY_USER_DISPLAY_NAME = "display-name"
|
||||
private const val MY_USER_AVATAR_URL = "avatar-url"
|
||||
private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
|
||||
|
||||
private val AN_EVENT_LIST = listOf<ProcessedEvent<NotifiableEvent>>()
|
||||
private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList(), emptyList())
|
||||
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk())
|
||||
private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed
|
||||
private val A_NOTIFICATION = mockk<Notification>()
|
||||
private val MESSAGE_META = RoomNotification.Message.Meta(
|
||||
summaryLine = "ignored",
|
||||
messageCount = 1,
|
||||
latestTimestamp = -1,
|
||||
roomId = A_ROOM_ID,
|
||||
shouldBing = false
|
||||
)
|
||||
private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
|
||||
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION)
|
||||
private val ONE_SHOT_NOTIFICATION =
|
||||
OneShotNotification(notification = A_NOTIFICATION, key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationRendererTest {
|
||||
private val mockkNotificationDisplayer = MockkNotificationDisplayer()
|
||||
private val mockkNotificationFactory = MockkNotificationFactory()
|
||||
private val notificationDisplayer = FakeNotificationDisplayer()
|
||||
|
||||
private val notificationCreator = FakeNotificationCreator()
|
||||
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
|
||||
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
||||
private val notificationDataFactory = DefaultNotificationDataFactory(
|
||||
notificationCreator = notificationCreator,
|
||||
roomGroupMessageCreator = roomGroupMessageCreator,
|
||||
summaryGroupMessageCreator = summaryGroupMessageCreator,
|
||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
private val notificationIdProvider = NotificationIdProvider()
|
||||
|
||||
private val notificationRenderer = NotificationRenderer(
|
||||
notificationIdProvider = notificationIdProvider,
|
||||
notificationDisplayer = mockkNotificationDisplayer.instance,
|
||||
notificationFactory = mockkNotificationFactory.instance,
|
||||
notificationDisplayer = notificationDisplayer,
|
||||
notificationDataFactory = notificationDataFactory,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given no notifications when rendering then cancels summary notification`() = runTest {
|
||||
givenNoNotifications()
|
||||
renderEventsAsNotifications(emptyList())
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
||||
mockkNotificationDisplayer.verifySummaryCancelled()
|
||||
mockkNotificationDisplayer.verifyNoOtherInteractions()
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
|
||||
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
notificationDisplayer.verifySummaryCancelled()
|
||||
}
|
||||
|
||||
@Test
|
||||
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(
|
||||
A_NOTIFICATION,
|
||||
MESSAGE_META
|
||||
)
|
||||
)
|
||||
roomGroupMessageCreator.createRoomMessageResult = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }
|
||||
|
||||
renderEventsAsNotifications(listOf(aNotifiableMessageEvent()))
|
||||
|
||||
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence(
|
||||
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))
|
||||
)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), A_NOTIFICATION)
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
|
||||
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest {
|
||||
givenNotifications(
|
||||
simpleNotifications = listOf(
|
||||
OneShotNotification.Append(
|
||||
A_NOTIFICATION,
|
||||
ONE_SHOT_META.copy(key = AN_EVENT_ID.value)
|
||||
)
|
||||
)
|
||||
notificationCreator.createSimpleNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification }
|
||||
|
||||
renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID)))
|
||||
|
||||
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence(
|
||||
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))
|
||||
)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
showNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
|
||||
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest {
|
||||
givenNotifications(
|
||||
simpleNotifications = listOf(
|
||||
OneShotNotification.Append(
|
||||
A_NOTIFICATION,
|
||||
ONE_SHOT_META.copy(key = A_ROOM_ID.value)
|
||||
)
|
||||
)
|
||||
notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification }
|
||||
|
||||
renderEventsAsNotifications(listOf(anInviteNotifiableEvent()))
|
||||
|
||||
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence(
|
||||
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))
|
||||
)
|
||||
|
||||
renderEventsAsNotifications()
|
||||
|
||||
mockkNotificationDisplayer.verifyInOrder {
|
||||
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
|
||||
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun renderEventsAsNotifications() {
|
||||
private suspend fun renderEventsAsNotifications(events: List<NotifiableEvent>) {
|
||||
notificationRenderer.render(
|
||||
MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
|
||||
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
|
||||
eventsToProcess = AN_EVENT_LIST,
|
||||
eventsToProcess = events,
|
||||
imageLoader = FakeImageLoader().getImageLoader(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenNoNotifications() {
|
||||
givenNotifications(emptyList(), emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION)
|
||||
}
|
||||
|
||||
private fun givenNotifications(
|
||||
roomNotifications: List<RoomNotification> = emptyList(),
|
||||
invitationNotifications: List<OneShotNotification> = emptyList(),
|
||||
simpleNotifications: List<OneShotNotification> = emptyList(),
|
||||
fallbackNotifications: List<OneShotNotification> = emptyList(),
|
||||
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
|
||||
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION
|
||||
) {
|
||||
mockkNotificationFactory.givenNotificationsFor(
|
||||
groupedEvents = A_PROCESSED_EVENTS,
|
||||
matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
|
||||
useCompleteNotificationFormat = useCompleteNotificationFormat,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
simpleNotifications = simpleNotifications,
|
||||
fallbackNotifications = fallbackNotifications,
|
||||
summaryNotification = summaryNotification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications.factories
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -29,23 +30,29 @@ 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.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
|
||||
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.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
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
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.SimpleNotifiableEvent
|
||||
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.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationCreatorTest {
|
||||
class DefaultNotificationCreatorTest {
|
||||
@Test
|
||||
fun `test createDiagnosticNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
@@ -212,15 +219,10 @@ class NotificationCreatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createMessagesListNotification`() {
|
||||
fun `test createMessagesListNotification`() = runTest {
|
||||
val sut = createNotificationCreator()
|
||||
aMatrixUser()
|
||||
val result = sut.createMessagesListNotification(
|
||||
messageStyle = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("name")
|
||||
.build()
|
||||
),
|
||||
roomInfo = RoomEventGroupInfo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
@@ -235,20 +237,19 @@ class NotificationCreatorTest {
|
||||
largeIcon = null,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
tickerText = "tickerText",
|
||||
currentUser = aMatrixUser(),
|
||||
existingNotification = null,
|
||||
imageLoader = FakeImageLoader().getImageLoader(),
|
||||
events = emptyList(),
|
||||
)
|
||||
result.commonAssertions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createMessagesListNotification should bing and thread`() {
|
||||
fun `test createMessagesListNotification should bing and thread`() = runTest {
|
||||
val sut = createNotificationCreator()
|
||||
aMatrixUser()
|
||||
val result = sut.createMessagesListNotification(
|
||||
messageStyle = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("name")
|
||||
.build()
|
||||
),
|
||||
roomInfo = RoomEventGroupInfo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
@@ -263,6 +264,10 @@ class NotificationCreatorTest {
|
||||
largeIcon = null,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
tickerText = "tickerText",
|
||||
currentUser = aMatrixUser(),
|
||||
existingNotification = null,
|
||||
imageLoader = FakeImageLoader().getImageLoader(),
|
||||
events = emptyList(),
|
||||
)
|
||||
result.commonAssertions()
|
||||
}
|
||||
@@ -280,9 +285,10 @@ class NotificationCreatorTest {
|
||||
fun createNotificationCreator(
|
||||
context: Context = RuntimeEnvironment.getApplication(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationChannels: NotificationChannels = createNotificationChannels()
|
||||
notificationChannels: NotificationChannels = createNotificationChannels(),
|
||||
bitmapLoader: NotificationBitmapLoader = NotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
|
||||
): NotificationCreator {
|
||||
return NotificationCreator(
|
||||
return DefaultNotificationCreator(
|
||||
context = context,
|
||||
notificationChannels = notificationChannels,
|
||||
stringProvider = FakeStringProvider("test"),
|
||||
@@ -305,10 +311,23 @@ fun createNotificationCreator(
|
||||
stringProvider = FakeStringProvider("QuickReplyActionFactory"),
|
||||
clock = FakeSystemClock(),
|
||||
),
|
||||
bitmapLoader = bitmapLoader,
|
||||
acceptInvitationActionFactory = AcceptInvitationActionFactory(
|
||||
context = context,
|
||||
actionIds = NotificationActionIds(buildMeta),
|
||||
stringProvider = FakeStringProvider("AcceptInvitationActionFactory"),
|
||||
clock = FakeSystemClock(),
|
||||
),
|
||||
rejectInvitationActionFactory = RejectInvitationActionFactory(
|
||||
context = context,
|
||||
actionIds = NotificationActionIds(buildMeta),
|
||||
stringProvider = FakeStringProvider("RejectInvitationActionFactory"),
|
||||
clock = FakeSystemClock(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotificationChannels(): NotificationChannels {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return NotificationChannels(context, FakeStringProvider(""))
|
||||
return NotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider(""))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.fake
|
||||
|
||||
import android.service.notification.StatusBarNotification
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
|
||||
|
||||
class FakeActiveNotificationsProvider(
|
||||
var activeNotifications: MutableList<StatusBarNotification> = mutableListOf(),
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||
return activeNotifications.firstOrNull()
|
||||
}
|
||||
|
||||
override fun count(sessionId: SessionId): Int {
|
||||
return activeNotifications.size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.fake
|
||||
|
||||
import android.app.Notification
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import coil.ImageLoader
|
||||
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.factories.NotificationCreator
|
||||
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.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
||||
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.lambdaAnyRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
||||
class FakeNotificationCreator(
|
||||
var createMessagesListNotificationResult: LambdaListAnyParamsRecorder<Notification> = lambdaAnyRecorder { A_NOTIFICATION },
|
||||
var createRoomInvitationNotificationResult: LambdaOneParamRecorder<InviteNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION },
|
||||
var createSimpleNotificationResult: LambdaOneParamRecorder<SimpleNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION },
|
||||
var createFallbackNotificationResult: LambdaOneParamRecorder<FallbackNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION },
|
||||
var createSummaryListNotificationResult: LambdaFiveParamsRecorder<MatrixUser, NotificationCompat.InboxStyle?, String, Boolean, Long, Notification> =
|
||||
lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
|
||||
var createDiagnosticNotificationResult: LambdaNoParamRecorder<Notification> = lambdaRecorder { -> A_NOTIFICATION },
|
||||
) : NotificationCreator {
|
||||
override suspend fun createMessagesListNotification(
|
||||
roomInfo: RoomEventGroupInfo,
|
||||
threadId: ThreadId?,
|
||||
largeIcon: Bitmap?,
|
||||
lastMessageTimestamp: Long,
|
||||
tickerText: String,
|
||||
currentUser: MatrixUser,
|
||||
existingNotification: Notification?,
|
||||
imageLoader: ImageLoader,
|
||||
events: List<NotifiableMessageEvent>
|
||||
): Notification {
|
||||
return createMessagesListNotificationResult(
|
||||
listOf(roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, currentUser, existingNotification, imageLoader, events)
|
||||
)
|
||||
}
|
||||
|
||||
override fun createRoomInvitationNotification(inviteNotifiableEvent: InviteNotifiableEvent): Notification {
|
||||
return createRoomInvitationNotificationResult(inviteNotifiableEvent)
|
||||
}
|
||||
|
||||
override fun createSimpleEventNotification(simpleNotifiableEvent: SimpleNotifiableEvent): Notification {
|
||||
return createSimpleNotificationResult(simpleNotifiableEvent)
|
||||
}
|
||||
|
||||
override fun createFallbackNotification(fallbackNotifiableEvent: FallbackNotifiableEvent): Notification {
|
||||
return createFallbackNotificationResult(fallbackNotifiableEvent)
|
||||
}
|
||||
|
||||
override fun createSummaryListNotification(
|
||||
currentUser: MatrixUser,
|
||||
style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
lastMessageTimestamp: Long
|
||||
): Notification {
|
||||
return createSummaryListNotificationResult(currentUser, style, compatSummary, noisy, lastMessageTimestamp)
|
||||
}
|
||||
|
||||
override fun createDiagnosticNotification(): Notification {
|
||||
return createDiagnosticNotificationResult()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import coil.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.OneShotNotification
|
||||
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.fixtures.A_NOTIFICATION
|
||||
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.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaSixParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
||||
class FakeNotificationDataFactory(
|
||||
var messageEventToNotificationsResult: LambdaThreeParamsRecorder<List<NotifiableMessageEvent>, MatrixUser, ImageLoader, List<RoomNotification>> =
|
||||
lambdaRecorder { _, _, _ -> emptyList() },
|
||||
var summaryToNotificationsResult: LambdaSixParamsRecorder<
|
||||
MatrixUser,
|
||||
List<RoomNotification>,
|
||||
List<OneShotNotification>,
|
||||
List<OneShotNotification>,
|
||||
List<OneShotNotification>,
|
||||
Boolean,
|
||||
SummaryNotification
|
||||
> = lambdaRecorder { _, _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
|
||||
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, List<OneShotNotification>> =
|
||||
lambdaRecorder { _ -> emptyList() },
|
||||
) : NotificationDataFactory {
|
||||
override suspend fun toNotifications(messages: List<NotifiableMessageEvent>, currentUser: MatrixUser, imageLoader: ImageLoader): List<RoomNotification> {
|
||||
return messageEventToNotificationsResult(messages, currentUser, imageLoader)
|
||||
}
|
||||
|
||||
@JvmName("toNotificationInvites")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification> {
|
||||
return inviteToNotificationsResult(invites)
|
||||
}
|
||||
|
||||
@JvmName("toNotificationSimpleEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification> {
|
||||
return simpleEventToNotificationsResult(simpleEvents)
|
||||
}
|
||||
|
||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||
return fallbackEventToNotificationsResult(fallback)
|
||||
}
|
||||
|
||||
override fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): SummaryNotification {
|
||||
return summaryToNotificationsResult(
|
||||
currentUser,
|
||||
roomNotifications,
|
||||
invitationNotifications,
|
||||
simpleNotifications,
|
||||
fallbackNotifications,
|
||||
useCompleteNotificationFormat
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
|
||||
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
||||
class FakeNotificationDisplayer(
|
||||
var showNotificationMessageResult: LambdaThreeParamsRecorder<String?, Int, Notification, Boolean> = lambdaRecorder { _, _, _ -> true },
|
||||
var cancelNotificationMessageResult: LambdaTwoParamsRecorder<String?, Int, Unit> = lambdaRecorder { _, _ -> },
|
||||
var displayDiagnosticNotificationResult: LambdaOneParamRecorder<Notification, Boolean> = lambdaRecorder { _ -> true },
|
||||
var dismissDiagnosticNotificationResult: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
|
||||
) : NotificationDisplayer {
|
||||
override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean {
|
||||
return showNotificationMessageResult(tag, id, notification)
|
||||
}
|
||||
|
||||
override fun cancelNotificationMessage(tag: String?, id: Int) {
|
||||
return cancelNotificationMessageResult(tag, id)
|
||||
}
|
||||
|
||||
override fun displayDiagnosticNotification(notification: Notification): Boolean {
|
||||
return displayDiagnosticNotificationResult(notification)
|
||||
}
|
||||
|
||||
override fun dismissDiagnosticNotification() {
|
||||
return dismissDiagnosticNotificationResult()
|
||||
}
|
||||
|
||||
fun verifySummaryCancelled(times: Int = 1) {
|
||||
cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence(
|
||||
listOf(value(null), value(NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,31 +16,27 @@
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
|
||||
import android.app.Notification
|
||||
import coil.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.fixtures.A_NOTIFICATION
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
||||
class MockkRoomGroupMessageCreator {
|
||||
val instance = mockk<RoomGroupMessageCreator>()
|
||||
|
||||
fun givenCreatesRoomMessageFor(
|
||||
matrixUser: MatrixUser,
|
||||
class FakeRoomGroupMessageCreator(
|
||||
var createRoomMessageResult: LambdaFiveParamsRecorder<MatrixUser, List<NotifiableMessageEvent>, RoomId, ImageLoader, Notification?, Notification> =
|
||||
lambdaRecorder { _, _, _, _, _, -> A_NOTIFICATION }
|
||||
) : RoomGroupMessageCreator {
|
||||
override suspend fun createRoomMessage(
|
||||
currentUser: MatrixUser,
|
||||
events: List<NotifiableMessageEvent>,
|
||||
roomId: RoomId,
|
||||
): RoomNotification.Message {
|
||||
val mockMessage = mockk<RoomNotification.Message>()
|
||||
coEvery {
|
||||
instance.createRoomMessage(
|
||||
currentUser = matrixUser,
|
||||
events = events,
|
||||
roomId = roomId,
|
||||
imageLoader = any(),
|
||||
)
|
||||
} returns mockMessage
|
||||
return mockMessage
|
||||
imageLoader: ImageLoader,
|
||||
existingNotification: Notification?
|
||||
): Notification {
|
||||
return createRoomMessageResult(currentUser, events, roomId, imageLoader, existingNotification)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import android.app.Notification
|
||||
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.RoomNotification
|
||||
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
|
||||
import io.element.android.tests.testutils.lambda.LambdaSixParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
||||
class FakeSummaryGroupMessageCreator(
|
||||
var createSummaryNotificationResult: LambdaSixParamsRecorder<
|
||||
MatrixUser, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, List<OneShotNotification>, Boolean, Notification
|
||||
> =
|
||||
lambdaRecorder { _, _, _, _, _, _ -> A_NOTIFICATION }
|
||||
) : SummaryGroupMessageCreator {
|
||||
override fun createSummaryNotification(
|
||||
currentUser: MatrixUser,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
useCompleteNotificationFormat: Boolean
|
||||
): Notification {
|
||||
return createSummaryNotificationResult(
|
||||
currentUser,
|
||||
roomNotifications,
|
||||
invitationNotifications,
|
||||
simpleNotifications,
|
||||
fallbackNotifications,
|
||||
useCompleteNotificationFormat
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class MockkNotificationCreator {
|
||||
val instance = mockk<NotificationCreator>()
|
||||
|
||||
fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
|
||||
val mockNotification = mockk<Notification>()
|
||||
every { instance.createRoomInvitationNotification(event) } returns mockNotification
|
||||
return mockNotification
|
||||
}
|
||||
|
||||
fun givenCreateSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification {
|
||||
val mockNotification = mockk<Notification>()
|
||||
every { instance.createSimpleEventNotification(event) } returns mockNotification
|
||||
return mockNotification
|
||||
}
|
||||
|
||||
fun givenCreateDiagnosticNotification(): Notification {
|
||||
val mockNotification = mockk<Notification>()
|
||||
every { instance.createDiagnosticNotification() } returns mockNotification
|
||||
return mockNotification
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
|
||||
class MockkNotificationDisplayer {
|
||||
val instance = mockk<NotificationDisplayer>(relaxed = true)
|
||||
|
||||
fun givenDisplayDiagnosticNotificationResult(result: Boolean) {
|
||||
every { instance.displayDiagnosticNotification(any()) } returns result
|
||||
}
|
||||
|
||||
fun verifySummaryCancelled() {
|
||||
verify { instance.cancelNotificationMessage(tag = null, NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)) }
|
||||
}
|
||||
|
||||
fun verifyNoOtherInteractions() {
|
||||
confirmVerified(instance)
|
||||
}
|
||||
|
||||
fun verifyInOrder(verifyBlock: NotificationDisplayer.() -> Unit) {
|
||||
verifyOrder { verifyBlock(instance) }
|
||||
verifyNoOtherInteractions()
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
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
|
||||
|
||||
class MockkNotificationFactory {
|
||||
val instance = mockk<NotificationFactory>()
|
||||
|
||||
fun givenNotificationsFor(
|
||||
groupedEvents: GroupedNotificationEvents,
|
||||
matrixUser: MatrixUser,
|
||||
useCompleteNotificationFormat: Boolean,
|
||||
roomNotifications: List<RoomNotification>,
|
||||
invitationNotifications: List<OneShotNotification>,
|
||||
simpleNotifications: List<OneShotNotification>,
|
||||
fallbackNotifications: List<OneShotNotification>,
|
||||
summaryNotification: SummaryNotification
|
||||
) {
|
||||
with(instance) {
|
||||
coEvery { groupedEvents.roomEvents.toNotifications(matrixUser, any()) } returns roomNotifications
|
||||
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
|
||||
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
|
||||
every { groupedEvents.fallbackEvents.toNotifications() } returns fallbackNotifications
|
||||
|
||||
every {
|
||||
createSummaryNotification(
|
||||
matrixUser,
|
||||
roomNotifications,
|
||||
invitationNotifications,
|
||||
simpleNotifications,
|
||||
fallbackNotifications,
|
||||
useCompleteNotificationFormat
|
||||
)
|
||||
} returns summaryNotification
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.fake
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.OutdatedEventDetector
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class MockkOutdatedEventDetector {
|
||||
val instance = mockk<OutdatedEventDetector>()
|
||||
|
||||
fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) {
|
||||
every { instance.isMessageOutdated(notifiableEvent) } returns true
|
||||
}
|
||||
|
||||
fun givenEventIsInDate(notifiableEvent: NotifiableEvent) {
|
||||
every { instance.isMessageOutdated(notifiableEvent) } returns false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
* Copyright (c) 2024 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.
|
||||
@@ -14,11 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
package io.element.android.libraries.push.impl.notifications.fixtures
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
|
||||
import io.mockk.mockk
|
||||
import android.app.Notification
|
||||
|
||||
class MockkSummaryGroupMessageCreator {
|
||||
val instance = mockk<SummaryGroupMessageCreator>()
|
||||
}
|
||||
val A_NOTIFICATION = Notification()
|
||||
@@ -18,27 +18,27 @@ package io.element.android.libraries.push.impl.troubleshoot
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer
|
||||
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.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationTestTest {
|
||||
private val mockkNotificationCreator = MockkNotificationCreator().apply {
|
||||
givenCreateDiagnosticNotification()
|
||||
}
|
||||
private val mockkNotificationDisplayer = MockkNotificationDisplayer().apply {
|
||||
givenDisplayDiagnosticNotificationResult(true)
|
||||
}
|
||||
private val notificationCreator = FakeNotificationCreator()
|
||||
private val fakeNotificationDisplayer = FakeNotificationDisplayer(
|
||||
displayDiagnosticNotificationResult = lambdaRecorder { _ -> true },
|
||||
dismissDiagnosticNotificationResult = lambdaRecorder { -> }
|
||||
)
|
||||
|
||||
private val notificationClickHandler = NotificationClickHandler()
|
||||
|
||||
@Test
|
||||
fun `test NotificationTest notification cannot be displayed`() = runTest {
|
||||
mockkNotificationDisplayer.givenDisplayDiagnosticNotificationResult(false)
|
||||
fakeNotificationDisplayer.displayDiagnosticNotificationResult = lambdaRecorder { _ -> false }
|
||||
val sut = createNotificationTest()
|
||||
launch {
|
||||
sut.run(this)
|
||||
@@ -81,8 +81,8 @@ class NotificationTestTest {
|
||||
|
||||
private fun createNotificationTest(): NotificationTest {
|
||||
return NotificationTest(
|
||||
notificationCreator = mockkNotificationCreator.instance,
|
||||
notificationDisplayer = mockkNotificationDisplayer.instance,
|
||||
notificationCreator = notificationCreator,
|
||||
notificationDisplayer = fakeNotificationDisplayer,
|
||||
notificationClickHandler = notificationClickHandler,
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ class FakeNotificationDrawerManager : NotificationDrawerManager {
|
||||
clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
|
||||
}
|
||||
|
||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId, doRender: Boolean) {
|
||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
val key = getMembershipNotificationKey(sessionId, roomId)
|
||||
clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ object Versions {
|
||||
val versionName = "$versionMajor.$versionMinor.$versionPatch"
|
||||
const val compileSdk = 34
|
||||
const val targetSdk = 33
|
||||
const val minSdk = 23
|
||||
const val minSdk = 24
|
||||
val javaCompileVersion = JavaVersion.VERSION_17
|
||||
val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,20 @@ inline fun <reified T1, reified T2, reified T3, reified T4, reified T5, reified
|
||||
return LambdaFiveParamsRecorder(ensureNeverCalled, block)
|
||||
}
|
||||
|
||||
inline fun <reified T1, reified T2, reified T3, reified T4, reified T5, reified T6, reified R> lambdaRecorder(
|
||||
ensureNeverCalled: Boolean = false,
|
||||
noinline block: (T1, T2, T3, T4, T5, T6) -> R
|
||||
): LambdaSixParamsRecorder<T1, T2, T3, T4, T5, T6, R> {
|
||||
return LambdaSixParamsRecorder(ensureNeverCalled, block)
|
||||
}
|
||||
|
||||
inline fun <reified R> lambdaAnyRecorder(
|
||||
ensureNeverCalled: Boolean = false,
|
||||
noinline block: (List<Any?>) -> R
|
||||
): LambdaListAnyParamsRecorder<R> {
|
||||
return LambdaListAnyParamsRecorder(ensureNeverCalled, block)
|
||||
}
|
||||
|
||||
class LambdaNoParamRecorder<out R>(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R {
|
||||
override fun invoke(): R {
|
||||
onInvoke()
|
||||
@@ -125,3 +139,53 @@ class LambdaFiveParamsRecorder<in T1, in T2, in T3, in T4, in T5, out R>(ensureN
|
||||
return block(p1, p2, p3, p4, p5)
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaSixParamsRecorder<in T1, in T2, in T3, in T4, in T5, in T6, out R>(
|
||||
ensureNeverCalled: Boolean,
|
||||
val block: (T1, T2, T3, T4, T5, T6) -> R,
|
||||
) : LambdaRecorder(ensureNeverCalled), (T1, T2, T3, T4, T5, T6) -> R {
|
||||
override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6): R {
|
||||
onInvoke(p1, p2, p3, p4, p5, p6)
|
||||
return block(p1, p2, p3, p4, p5, p6)
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaSevenParamsRecorder<in T1, in T2, in T3, in T4, in T5, in T6, in T7, out R>(
|
||||
ensureNeverCalled: Boolean,
|
||||
val block: (T1, T2, T3, T4, T5, T6, T7) -> R,
|
||||
) : LambdaRecorder(ensureNeverCalled), (T1, T2, T3, T4, T5, T6, T7) -> R {
|
||||
override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6, p7: T7): R {
|
||||
onInvoke(p1, p2, p3, p4, p5, p6, p7)
|
||||
return block(p1, p2, p3, p4, p5, p6, p7)
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaEightParamsRecorder<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, out R>(
|
||||
ensureNeverCalled: Boolean,
|
||||
val block: (T1, T2, T3, T4, T5, T6, T7, T8) -> R,
|
||||
) : LambdaRecorder(ensureNeverCalled), (T1, T2, T3, T4, T5, T6, T7, T8) -> R {
|
||||
override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6, p7: T7, p8: T8): R {
|
||||
onInvoke(p1, p2, p3, p4, p5, p6, p7, p8)
|
||||
return block(p1, p2, p3, p4, p5, p6, p7, p8)
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaNineParamsRecorder<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, out R>(
|
||||
ensureNeverCalled: Boolean,
|
||||
val block: (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R,
|
||||
) : LambdaRecorder(ensureNeverCalled), (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R {
|
||||
override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5, p6: T6, p7: T7, p8: T8, p9: T9): R {
|
||||
onInvoke(p1, p2, p3, p4, p5, p6, p7, p8, p9)
|
||||
return block(p1, p2, p3, p4, p5, p6, p7, p8, p9)
|
||||
}
|
||||
}
|
||||
|
||||
class LambdaListAnyParamsRecorder<out R>(
|
||||
ensureNeverCalled: Boolean,
|
||||
val block: (List<Any?>) -> R,
|
||||
) : LambdaRecorder(ensureNeverCalled), (List<Any?>) -> R {
|
||||
override fun invoke(p: List<Any?>): R {
|
||||
onInvoke(*p.toTypedArray())
|
||||
return block(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,12 @@ fun any() = object : ParameterMatcher {
|
||||
override fun match(param: Any?) = true
|
||||
override fun toString(): String = "any()"
|
||||
}
|
||||
|
||||
/**
|
||||
* A matcher that matches any non null value
|
||||
* Can be used when we don't care about the value of a parameter, just about its nullability.
|
||||
*/
|
||||
fun nonNull() = object : ParameterMatcher {
|
||||
override fun match(param: Any?) = param != null
|
||||
override fun toString(): String = "nonNull()"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user