Add some tests (mainly imported from EA). Also change type from String to SessionId, RoomId, etc.

This commit is contained in:
Benoit Marty 2023-04-04 17:33:22 +02:00 committed by Benoit Marty
parent 31ff2e7e0b
commit 7e7aca4a53
54 changed files with 1569 additions and 151 deletions

View file

@ -21,6 +21,9 @@ import android.content.Intent
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
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.intent.IntentProvider import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity import io.element.android.x.MainActivity
import javax.inject.Inject import javax.inject.Inject
@ -34,7 +37,7 @@ class IntentProviderImpl @Inject constructor(
return Intent(context, MainActivity::class.java) return Intent(context, MainActivity::class.java)
} }
override fun getIntent(sessionId: String, roomId: String?, threadId: String?): Intent { override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
// TODO Handle deeplink or pass parameters // TODO Handle deeplink or pass parameters
return Intent(context, MainActivity::class.java) return Intent(context, MainActivity::class.java)
} }

View file

@ -0,0 +1,71 @@
/*
* 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.core.cache
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class CircularCacheTest {
@Test
fun `when putting more than cache size then cache is limited to cache size`() {
val (cache, internalData) = createIntCache(cacheSize = 3)
cache.putInOrder(1, 1, 1, 1, 1, 1)
assertThat(internalData).isEqualTo(arrayOf(1, 1, 1))
}
@Test
fun `when putting more than cache then acts as FIFO`() {
val (cache, internalData) = createIntCache(cacheSize = 3)
cache.putInOrder(1, 2, 3, 4)
assertThat(internalData).isEqualTo(arrayOf(4, 2, 3))
}
@Test
fun `given empty cache when checking if contains key then is false`() {
val (cache, _) = createIntCache(cacheSize = 3)
val result = cache.contains(1)
assertThat(result).isFalse()
}
@Test
fun `given cached key when checking if contains key then is true`() {
val (cache, _) = createIntCache(cacheSize = 3)
cache.put(1)
val result = cache.contains(1)
assertThat(result).isTrue()
}
private fun createIntCache(cacheSize: Int): Pair<CircularCache<Int>, Array<Int?>> {
var internalData: Array<Int?>? = null
val factory: (Int) -> Array<Int?> = {
Array<Int?>(it) { null }.also { array -> internalData = array }
}
return CircularCache(cacheSize, factory) to internalData!!
}
private fun CircularCache<Int>.putInOrder(vararg values: Int) {
values.forEach { put(it) }
}
}

View file

@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline @JvmInline
value class EventId(val value: String) : Serializable value class EventId(val value: String) : Serializable
fun String.asEventId() = EventId(this)

View file

@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline @JvmInline
value class RoomId(val value: String) : Serializable value class RoomId(val value: String) : Serializable
fun String.asRoomId() = RoomId(this)

View file

@ -17,3 +17,5 @@
package io.element.android.libraries.matrix.api.core package io.element.android.libraries.matrix.api.core
typealias SessionId = UserId typealias SessionId = UserId
fun String.asSessionId() = SessionId(this)

View file

@ -25,3 +25,5 @@ value class SpaceId(val value: String) : Serializable
* Value to use when no space is selected by the user. * Value to use when no space is selected by the user.
*/ */
val MAIN_SPACE = SpaceId("!mainSpace") val MAIN_SPACE = SpaceId("!mainSpace")
fun String.asSpaceId() = SpaceId(this)

View file

@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline @JvmInline
value class ThreadId(val value: String) : Serializable value class ThreadId(val value: String) : Serializable
fun String.asThreadId() = ThreadId(this)

View file

@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline @JvmInline
value class UserId(val value: String) : Serializable value class UserId(val value: String) : Serializable
fun String.asUserId() = UserId(this)

View file

@ -16,6 +16,10 @@
package io.element.android.libraries.matrix.api.notification package io.element.android.libraries.matrix.api.notification
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
interface NotificationService { interface NotificationService {
suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?> suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
} }

View file

@ -17,6 +17,9 @@
package io.element.android.libraries.matrix.impl.notification package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -28,15 +31,15 @@ class RustNotificationService(
) : NotificationService { ) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper() private val notificationMapper: NotificationMapper = NotificationMapper()
override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?> { override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
runCatching { runCatching {
org.matrix.rustcomponents.sdk.NotificationService( org.matrix.rustcomponents.sdk.NotificationService(
basePath = File(baseDirectory, "sessions").absolutePath, basePath = File(baseDirectory, "sessions").absolutePath,
userId = userId userId = userId.value
).use { ).use {
// TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628 // TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628
it.getNotificationItem(roomId, eventId)?.let { notificationItem -> it.getNotificationItem(roomId.value, eventId.value)?.let { notificationItem ->
notificationMapper.map(notificationItem) notificationMapper.map(notificationItem)
} }
} }

View file

@ -31,8 +31,10 @@ val A_USER_ID = UserId("@alice:server.org")
val A_SESSION_ID = SessionId(A_USER_ID.value) val A_SESSION_ID = SessionId(A_USER_ID.value)
val A_SPACE_ID = SpaceId("!aSpaceId") val A_SPACE_ID = SpaceId("!aSpaceId")
val A_ROOM_ID = RoomId("!aRoomId") val A_ROOM_ID = RoomId("!aRoomId")
val A_ROOM_ID_2 = RoomId("!aRoomId2")
val A_THREAD_ID = ThreadId("\$aThreadId") val A_THREAD_ID = ThreadId("\$aThreadId")
val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID = EventId("\$anEventId")
val AN_EVENT_ID_2 = EventId("\$anEventId2")
const val A_UNIQUE_ID = "aUniqueId" const val A_UNIQUE_ID = "aUniqueId"
const val A_ROOM_NAME = "A room name" const val A_ROOM_NAME = "A room name"

View file

@ -16,11 +16,14 @@
package io.element.android.libraries.matrix.test.notification package io.element.android.libraries.matrix.test.notification
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.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
class FakeNotificationService : NotificationService { class FakeNotificationService : NotificationService {
override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?> { override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
return Result.success(null) return Result.success(null)
} }
} }

View file

@ -54,7 +54,6 @@ dependencies {
exclude(group = "com.android.support", module = "support-annotations") exclude(group = "com.android.support", module = "support-annotations")
} }
implementation(platform(libs.google.firebase.bom)) implementation(platform(libs.google.firebase.bom))
implementation("com.google.firebase:firebase-messaging-ktx") implementation("com.google.firebase:firebase-messaging-ktx")
@ -62,7 +61,10 @@ dependencies {
api("com.github.UnifiedPush:android-connector:2.1.1") api("com.github.UnifiedPush:android-connector:2.1.1")
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.mockk)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.appnavstate.test)
} }

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.clientsecret.PushClientSecret
@ -98,7 +99,7 @@ class PushersManager @Inject constructor(
} else { } else {
// Register the pusher to the server // Register the pusher to the server
matrixClient.pushersService().setHttpPusher( matrixClient.pushersService().setHttpPusher(
createHttpPusher(pushKey, gateway, matrixClient.sessionId.value) createHttpPusher(pushKey, gateway, matrixClient.sessionId)
).fold( ).fold(
{ {
userDataStore.setCurrentRegisteredPushKey(pushKey) userDataStore.setCurrentRegisteredPushKey(pushKey)
@ -113,7 +114,7 @@ class PushersManager @Inject constructor(
private suspend fun createHttpPusher( private suspend fun createHttpPusher(
pushKey: String, pushKey: String,
gateway: String, gateway: String,
userId: String, userId: SessionId,
): SetHttpPusherData = ): SetHttpPusherData =
SetHttpPusherData( SetHttpPusherData(
pushKey = pushKey, pushKey = pushKey,
@ -167,6 +168,6 @@ class PushersManager @Inject constructor(
} }
companion object { companion object {
const val TEST_EVENT_ID = "\$THIS_IS_A_FAKE_EVENT_ID" val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
} }
} }

View file

@ -16,20 +16,22 @@
package io.element.android.libraries.push.impl.clientsecret package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecret { interface PushClientSecret {
/** /**
* To call when registering a pusher. It will return the existing secret or create a new one. * To call when registering a pusher. It will return the existing secret or create a new one.
*/ */
suspend fun getSecretForUser(userId: String): String suspend fun getSecretForUser(userId: SessionId): String
/** /**
* To call when receiving a push containing a client secret. * To call when receiving a push containing a client secret.
* Return null if not found. * Return null if not found.
*/ */
suspend fun getUserIdFromSecret(clientSecret: String): String? suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
/** /**
* To call when the user signs out. * To call when the user signs out.
*/ */
suspend fun resetSecretForUser(userId: String) suspend fun resetSecretForUser(userId: SessionId)
} }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.clientsecret
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@ -25,7 +26,7 @@ class PushClientSecretImpl @Inject constructor(
private val pushClientSecretFactory: PushClientSecretFactory, private val pushClientSecretFactory: PushClientSecretFactory,
private val pushClientSecretStore: PushClientSecretStore, private val pushClientSecretStore: PushClientSecretStore,
) : PushClientSecret { ) : PushClientSecret {
override suspend fun getSecretForUser(userId: String): String { override suspend fun getSecretForUser(userId: SessionId): String {
val existingSecret = pushClientSecretStore.getSecret(userId) val existingSecret = pushClientSecretStore.getSecret(userId)
if (existingSecret != null) { if (existingSecret != null) {
return existingSecret return existingSecret
@ -35,11 +36,11 @@ class PushClientSecretImpl @Inject constructor(
return newSecret return newSecret
} }
override suspend fun getUserIdFromSecret(clientSecret: String): String? { override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return pushClientSecretStore.getUserIdFromSecret(clientSecret) return pushClientSecretStore.getUserIdFromSecret(clientSecret)
} }
override suspend fun resetSecretForUser(userId: String) { override suspend fun resetSecretForUser(userId: SessionId) {
pushClientSecretStore.resetSecret(userId) pushClientSecretStore.resetSecret(userId)
} }
} }

View file

@ -16,9 +16,11 @@
package io.element.android.libraries.push.impl.clientsecret package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecretStore { interface PushClientSecretStore {
suspend fun storeSecret(userId: String, clientSecret: String) suspend fun storeSecret(userId: SessionId, clientSecret: String)
suspend fun getSecret(userId: String): String? suspend fun getSecret(userId: SessionId): String?
suspend fun resetSecret(userId: String) suspend fun resetSecret(userId: SessionId)
suspend fun getUserIdFromSecret(clientSecret: String): String? suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
} }

View file

@ -25,6 +25,8 @@ import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.asSessionId
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import javax.inject.Inject import javax.inject.Inject
@ -34,29 +36,29 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
class PushClientSecretStoreDataStore @Inject constructor( class PushClientSecretStoreDataStore @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : PushClientSecretStore { ) : PushClientSecretStore {
override suspend fun storeSecret(userId: String, clientSecret: String) { override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
context.dataStore.edit { settings -> context.dataStore.edit { settings ->
settings[getPreferenceKeyForUser(userId)] = clientSecret settings[getPreferenceKeyForUser(userId)] = clientSecret
} }
} }
override suspend fun getSecret(userId: String): String? { override suspend fun getSecret(userId: SessionId): String? {
return context.dataStore.data.first()[getPreferenceKeyForUser(userId)] return context.dataStore.data.first()[getPreferenceKeyForUser(userId)]
} }
override suspend fun resetSecret(userId: String) { override suspend fun resetSecret(userId: SessionId) {
context.dataStore.edit { settings -> context.dataStore.edit { settings ->
settings.remove(getPreferenceKeyForUser(userId)) settings.remove(getPreferenceKeyForUser(userId))
} }
} }
override suspend fun getUserIdFromSecret(clientSecret: String): String? { override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
val keyValues = context.dataStore.data.first().asMap() val keyValues = context.dataStore.data.first().asMap()
val matchingKey = keyValues.keys.firstOrNull { val matchingKey = keyValues.keys.firstOrNull {
keyValues[it] == clientSecret keyValues[it] == clientSecret
} }
return matchingKey?.name return matchingKey?.name?.asSessionId()
} }
private fun getPreferenceKeyForUser(userId: String) = stringPreferencesKey(userId) private fun getPreferenceKeyForUser(userId: SessionId) = stringPreferencesKey(userId.value)
} }

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.push.impl.firebase package io.element.android.libraries.push.impl.firebase
import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.push.impl.push.PushData import io.element.android.libraries.push.impl.push.PushData
/** /**
@ -40,8 +42,8 @@ data class PushDataFirebase(
) )
fun PushDataFirebase.toPushData() = PushData( fun PushDataFirebase.toPushData() = PushData(
eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
unread = unread, unread = unread,
clientSecret = clientSecret, clientSecret = clientSecret,
) )

View file

@ -17,6 +17,9 @@
package io.element.android.libraries.push.impl.intent package io.element.android.libraries.push.impl.intent
import android.content.Intent import android.content.Intent
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
interface IntentProvider { interface IntentProvider {
/** /**
@ -25,8 +28,8 @@ interface IntentProvider {
fun getMainIntent(): Intent fun getMainIntent(): Intent
fun getIntent( fun getIntent(
sessionId: String, sessionId: SessionId,
roomId: String?, roomId: RoomId?,
threadId: String?, threadId: ThreadId?,
): Intent ): Intent
} }

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
@ -52,13 +53,13 @@ class NotifiableEventResolver @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) { ) {
suspend fun resolveEvent(userId: String, roomId: String, eventId: String): NotifiableEvent? { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session // Restore session
val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return null val session = matrixAuthenticationService.restoreSession(sessionId).getOrNull() ?: return null
// TODO EAx, no need for a session? // TODO EAx, no need for a session?
val notificationData = session.let {// TODO Use make the app crashes val notificationData = session.let {// TODO Use make the app crashes
it.notificationService().getNotification( it.notificationService().getNotification(
userId = userId, userId = sessionId,
roomId = roomId, roomId = roomId,
eventId = eventId, eventId = eventId,
) )
@ -72,11 +73,11 @@ class NotifiableEventResolver @Inject constructor(
} }
).orDefault(roomId, eventId) ).orDefault(roomId, eventId)
return notificationData.asNotifiableEvent(userId, roomId, eventId) return notificationData.asNotifiableEvent(sessionId, roomId, eventId)
} }
} }
private fun NotificationData.asNotifiableEvent(userId: String, roomId: String, eventId: String): NotifiableEvent { private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent {
return NotifiableMessageEvent( return NotifiableMessageEvent(
sessionId = userId, sessionId = userId,
roomId = roomId, roomId = roomId,
@ -105,12 +106,12 @@ private fun NotificationData.asNotifiableEvent(userId: String, roomId: String, e
/** /**
* TODO This is a temporary method for EAx * TODO This is a temporary method for EAx
*/ */
private fun NotificationData?.orDefault(roomId: String, eventId: String): NotificationData { private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData( return this ?: NotificationData(
item = MatrixTimelineItem.Event( item = MatrixTimelineItem.Event(
event = EventTimelineItem( event = EventTimelineItem(
uniqueIdentifier = eventId, uniqueIdentifier = eventId.value,
eventId = EventId(eventId), eventId = eventId,
isEditable = false, isEditable = false,
isLocal = false, isLocal = false,
isOwn = false, isOwn = false,
@ -121,18 +122,18 @@ private fun NotificationData?.orDefault(roomId: String, eventId: String): Notifi
senderProfile = ProfileTimelineDetails.Unavailable, senderProfile = ProfileTimelineDetails.Unavailable,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
content = MessageContent( content = MessageContent(
body = eventId, body = eventId.value,
inReplyTo = null, inReplyTo = null,
isEdited = false, isEdited = false,
type = TextMessageType( type = TextMessageType(
body = eventId, body = eventId.value,
formatted = null formatted = null
) )
) )
), ),
), ),
title = roomId, title = roomId.value,
subtitle = eventId, subtitle = eventId.value,
isNoisy = false, isNoisy = false,
avatarUrl = null, avatarUrl = null,
) )

View file

@ -22,6 +22,11 @@ import android.content.Intent
import androidx.core.app.RemoteInput import androidx.core.app.RemoteInput
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.matrix.api.core.asSessionId
import io.element.android.libraries.matrix.api.core.asThreadId
import io.element.android.libraries.push.impl.log.notificationLoggerTag import io.element.android.libraries.push.impl.log.notificationLoggerTag
import io.element.android.services.analytics.api.AnalyticsTracker import io.element.android.services.analytics.api.AnalyticsTracker
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
@ -46,29 +51,29 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
if (intent == null || context == null) return if (intent == null || context == null) return
context.bindings<NotificationBroadcastReceiverBindings>().inject(this) context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent") Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent")
val sessionId = intent.extras?.getString(KEY_SESSION_ID) ?: return val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.asSessionId() ?: return
when (intent.action) { when (intent.action) {
actionIds.smartReply -> actionIds.smartReply ->
handleSmartReply(intent, context) handleSmartReply(intent, context)
actionIds.dismissRoom -> actionIds.dismissRoom ->
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) } notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) }
} }
actionIds.dismissSummary -> actionIds.dismissSummary ->
notificationDrawerManager.clearAllEvents(sessionId) notificationDrawerManager.clearAllEvents(sessionId)
actionIds.markRoomRead -> actionIds.markRoomRead ->
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) } notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) }
handleMarkAsRead(sessionId, roomId) handleMarkAsRead(sessionId, roomId)
} }
actionIds.join -> { actionIds.join -> {
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) }
handleJoinRoom(sessionId, roomId) handleJoinRoom(sessionId, roomId)
} }
} }
actionIds.reject -> { actionIds.reject -> {
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) } notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) }
handleRejectRoom(sessionId, roomId) handleRejectRoom(sessionId, roomId)
} }
@ -76,7 +81,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
} }
} }
private fun handleJoinRoom(sessionId: String, roomId: String) { private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) {
/* /*
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
val room = session.getRoom(roomId) val room = session.getRoom(roomId)
@ -93,7 +98,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/ */
} }
private fun handleRejectRoom(sessionId: String, roomId: String) { private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) {
/* /*
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
session.coroutineScope.launch { session.coroutineScope.launch {
@ -104,7 +109,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/ */
} }
private fun handleMarkAsRead(sessionId: String, roomId: String) { private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) {
/* /*
activeSessionHolder.getActiveSession().let { session -> activeSessionHolder.getActiveSession().let { session ->
val room = session.getRoom(roomId) val room = session.getRoom(roomId)
@ -120,11 +125,11 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) { private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent) val message = getReplyMessage(intent)
val sessionId = intent.getStringExtra(KEY_SESSION_ID) val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.asSessionId()
val roomId = intent.getStringExtra(KEY_ROOM_ID) val roomId = intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()
val threadId = intent.getStringExtra(KEY_THREAD_ID) val threadId = intent.getStringExtra(KEY_THREAD_ID)?.asThreadId()
if (message.isNullOrBlank() || roomId.isNullOrBlank()) { if (message.isNullOrBlank() || roomId == null) {
// ignore this event // ignore this event
// Can this happen? should we update notification? // Can this happen? should we update notification?
return return

View file

@ -25,6 +25,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
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.api.store.PushDataStore import io.element.android.libraries.push.api.store.PushDataStore
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
@ -86,13 +89,13 @@ class NotificationDrawerManager @Inject constructor(
is AppNavigationState.Space -> {} is AppNavigationState.Space -> {}
is AppNavigationState.Room -> { is AppNavigationState.Room -> {
// Cleanup notification for current room // Cleanup notification for current room
onEnteringRoom(appNavigationState.parentSpace.parentSession.sessionId.value, appNavigationState.roomId.value) onEnteringRoom(appNavigationState.parentSpace.parentSession.sessionId, appNavigationState.roomId)
} }
is AppNavigationState.Thread -> { is AppNavigationState.Thread -> {
onEnteringThread( onEnteringThread(
appNavigationState.parentRoom.parentSpace.parentSession.sessionId.value, appNavigationState.parentRoom.parentSpace.parentSession.sessionId,
appNavigationState.parentRoom.roomId.value, appNavigationState.parentRoom.roomId,
appNavigationState.threadId.value appNavigationState.threadId
) )
} }
} }
@ -136,7 +139,7 @@ class NotificationDrawerManager @Inject constructor(
/** /**
* Clear all known events and refresh the notification drawer. * Clear all known events and refresh the notification drawer.
*/ */
fun clearAllEvents(sessionId: String) { fun clearAllEvents(sessionId: SessionId) {
updateEvents { it.clearMessagesForSession(sessionId) } updateEvents { it.clearMessagesForSession(sessionId) }
} }
@ -144,7 +147,7 @@ class NotificationDrawerManager @Inject constructor(
* 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. * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
*/ */
private fun onEnteringRoom(sessionId: String, roomId: String) { private fun onEnteringRoom(sessionId: SessionId, roomId: RoomId) {
updateEvents { updateEvents {
it.clearMessagesForRoom(sessionId, roomId) it.clearMessagesForRoom(sessionId, roomId)
} }
@ -154,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(
* Should be called when the application is currently opened and showing timeline for the given threadId. * 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. * Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room.
*/ */
private fun onEnteringThread(sessionId: String, roomId: String, threadId: String) { private fun onEnteringThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
updateEvents { updateEvents {
it.clearMessagesForThread(sessionId, roomId, threadId) it.clearMessagesForThread(sessionId, roomId, threadId)
} }

View file

@ -17,6 +17,10 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.core.cache.CircularCache 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.InviteNotifiableEvent 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.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -30,10 +34,10 @@ data class NotificationEventQueue constructor(
* Acts as a notification debouncer to stop already dismissed push notifications from * Acts as a notification debouncer to stop already dismissed push notifications from
* displaying again when the /sync response is delayed. * displaying again when the /sync response is delayed.
*/ */
private val seenEventIds: CircularCache<String> private val seenEventIds: CircularCache<EventId>
) { ) {
fun markRedacted(eventIds: List<String>) { fun markRedacted(eventIds: List<EventId>) {
eventIds.forEach { redactedId -> eventIds.forEach { redactedId ->
queue.replace(redactedId) { queue.replace(redactedId) {
when (it) { when (it) {
@ -45,7 +49,8 @@ data class NotificationEventQueue constructor(
} }
} }
fun syncRoomEvents(roomsLeft: Collection<String>, roomsJoined: Collection<String>) { // TODO EAx call this
fun syncRoomEvents(roomsLeft: Collection<RoomId>, roomsJoined: Collection<RoomId>) {
if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) { if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) {
queue.removeAll { queue.removeAll {
when (it) { when (it) {
@ -125,30 +130,30 @@ data class NotificationEventQueue constructor(
) )
} }
fun clearMemberShipNotificationForRoom(sessionId: String, roomId: String) { fun clearMemberShipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMemberShipOfRoom $sessionId, $roomId") Timber.d("clearMemberShipOfRoom $sessionId, $roomId")
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId } queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId }
} }
fun clearMessagesForSession(sessionId: String) { fun clearMessagesForSession(sessionId: SessionId) {
Timber.d("clearMessagesForSession $sessionId") Timber.d("clearMessagesForSession $sessionId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId} queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId }
} }
fun clearMessagesForRoom(sessionId: String, roomId: String) { fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMessageEventOfRoom $sessionId, $roomId") Timber.d("clearMessageEventOfRoom $sessionId, $roomId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId } queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }
} }
fun clearMessagesForThread(sessionId: String, roomId: String, threadId: String) { fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
Timber.d("clearMessageEventOfThread $sessionId, $roomId, $threadId") Timber.d("clearMessageEventOfThread $sessionId, $roomId, $threadId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId } queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId }
} }
fun rawEvents(): List<NotifiableEvent> = queue fun rawEvents(): List<NotifiableEvent> = queue
} }
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { private fun MutableList<NotifiableEvent>.replace(eventId: EventId, block: (NotifiableEvent) -> NotifiableEvent) {
val indexToReplace = indexOfFirst { it.eventId == eventId } val indexToReplace = indexOfFirst { it.eventId == eventId }
if (indexToReplace == -1) { if (indexToReplace == -1) {
return return

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import 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.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
@ -30,8 +32,8 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) { ) {
fun Map<String, ProcessedMessageEvents>.toNotifications( fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
sessionId: String, sessionId: SessionId,
myUserDisplayName: String, myUserDisplayName: String,
myUserAvatarUrl: String? myUserAvatarUrl: String?
): List<RoomNotification> { ): List<RoomNotification> {
@ -62,11 +64,11 @@ class NotificationFactory @Inject constructor(
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(): List<OneShotNotification> { fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
return map { (processed, event) -> return map { (processed, event) ->
when (processed) { when (processed) {
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId) ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
ProcessedEvent.Type.KEEP -> OneShotNotification.Append( ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
notificationUtils.buildRoomInvitationNotification(event), notificationUtils.buildRoomInvitationNotification(event),
OneShotNotification.Append.Meta( OneShotNotification.Append.Meta(
key = event.roomId, key = event.roomId.value,
summaryLine = event.description, summaryLine = event.description,
isNoisy = event.noisy, isNoisy = event.noisy,
timestamp = event.timestamp timestamp = event.timestamp
@ -80,11 +82,11 @@ class NotificationFactory @Inject constructor(
fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(): List<OneShotNotification> { fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
return map { (processed, event) -> return map { (processed, event) ->
when (processed) { when (processed) {
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId) ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
ProcessedEvent.Type.KEEP -> OneShotNotification.Append( ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
notificationUtils.buildSimpleEventNotification(event), notificationUtils.buildSimpleEventNotification(event),
OneShotNotification.Append.Meta( OneShotNotification.Append.Meta(
key = event.eventId, key = event.eventId.value,
summaryLine = event.description, summaryLine = event.description,
isNoisy = event.noisy, isNoisy = event.noisy,
timestamp = event.timestamp timestamp = event.timestamp
@ -95,7 +97,7 @@ class NotificationFactory @Inject constructor(
} }
fun createSummaryNotification( fun createSummaryNotification(
sessionId: String, sessionId: SessionId,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
@ -120,10 +122,10 @@ class NotificationFactory @Inject constructor(
} }
sealed interface RoomNotification { sealed interface RoomNotification {
data class Removed(val roomId: String) : RoomNotification data class Removed(val roomId: RoomId) : RoomNotification
data class Message(val notification: Notification, val meta: Meta) : RoomNotification { data class Message(val notification: Notification, val meta: Meta) : RoomNotification {
data class Meta( data class Meta(
val roomId: String, val roomId: RoomId,
val summaryLine: CharSequence, val summaryLine: CharSequence,
val messageCount: Int, val messageCount: Int,
val latestTimestamp: Long, val latestTimestamp: Long,

View file

@ -18,27 +18,28 @@ package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject import javax.inject.Inject
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class NotificationIdProvider @Inject constructor() { class NotificationIdProvider @Inject constructor() {
fun getSummaryNotificationId(sessionId: String): Int { fun getSummaryNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
} }
fun getRoomMessagesNotificationId(sessionId: String): Int { fun getRoomMessagesNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_MESSAGES_NOTIFICATION_ID return getOffset(sessionId) + ROOM_MESSAGES_NOTIFICATION_ID
} }
fun getRoomEventNotificationId(sessionId: String): Int { fun getRoomEventNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_EVENT_NOTIFICATION_ID return getOffset(sessionId) + ROOM_EVENT_NOTIFICATION_ID
} }
fun getRoomInvitationNotificationId(sessionId: String): Int { fun getRoomInvitationNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID
} }
private fun getOffset(sessionId: String): Int { private fun getOffset(sessionId: SessionId): Int {
// TODO EAx multi account: return different value for users and persist data // TODO EAx multi account: return different value for users and persist data
return 0 return 0
} }

View file

@ -16,6 +16,8 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent 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.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -31,7 +33,7 @@ class NotificationRenderer @Inject constructor(
@WorkerThread @WorkerThread
fun render( fun render(
sessionId: String, sessionId: SessionId,
myUserDisplayName: String, myUserDisplayName: String,
myUserAvatarUrl: String?, myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean, useCompleteNotificationFormat: Boolean,
@ -60,12 +62,12 @@ class NotificationRenderer @Inject constructor(
when (wrapper) { when (wrapper) {
is RoomNotification.Removed -> { is RoomNotification.Removed -> {
Timber.d("Removing room messages notification ${wrapper.roomId}") Timber.d("Removing room messages notification ${wrapper.roomId}")
notificationDisplayer.cancelNotificationMessage(wrapper.roomId, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
} }
is RoomNotification.Message -> if (useCompleteNotificationFormat) { is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}") Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotificationMessage(
wrapper.meta.roomId, wrapper.meta.roomId.value,
notificationIdProvider.getRoomMessagesNotificationId(sessionId), notificationIdProvider.getRoomMessagesNotificationId(sessionId),
wrapper.notification wrapper.notification
) )
@ -125,7 +127,7 @@ class NotificationRenderer @Inject constructor(
} }
private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents { private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents {
val roomIdToEventMap: MutableMap<String, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap() val roomIdToEventMap: MutableMap<RoomId, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap()
val simpleEvents: MutableList<ProcessedEvent<SimpleNotifiableEvent>> = ArrayList() val simpleEvents: MutableList<ProcessedEvent<SimpleNotifiableEvent>> = ArrayList()
val invitationEvents: MutableList<ProcessedEvent<InviteNotifiableEvent>> = ArrayList() val invitationEvents: MutableList<ProcessedEvent<InviteNotifiableEvent>> = ArrayList()
forEach { forEach {
@ -145,7 +147,7 @@ private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotifica
private fun <T : NotifiableEvent> ProcessedEvent<NotifiableEvent>.castedToEventType(): ProcessedEvent<T> = this as ProcessedEvent<T> private fun <T : NotifiableEvent> ProcessedEvent<NotifiableEvent>.castedToEventType(): ProcessedEvent<T> = this as ProcessedEvent<T>
data class GroupedNotificationEvents( data class GroupedNotificationEvents(
val roomEvents: Map<String, List<ProcessedEvent<NotifiableMessageEvent>>>, val roomEvents: Map<RoomId, List<ProcessedEvent<NotifiableMessageEvent>>>,
val simpleEvents: List<ProcessedEvent<SimpleNotifiableEvent>>, val simpleEvents: List<ProcessedEvent<SimpleNotifiableEvent>>,
val invitationEvents: List<ProcessedEvent<InviteNotifiableEvent>> val invitationEvents: List<ProcessedEvent<InviteNotifiableEvent>>
) )

View file

@ -44,6 +44,9 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
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.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.intent.IntentProvider import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -215,7 +218,7 @@ class NotificationUtils @Inject constructor(
fun buildMessagesListNotification( fun buildMessagesListNotification(
messageStyle: NotificationCompat.MessagingStyle, messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
threadId: String?, threadId: ThreadId?,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?, senderDisplayNameForReplyCompat: String?,
@ -244,7 +247,7 @@ class NotificationUtils @Inject constructor(
// that can be displayed in not disturb mode if white listed (the later will need compat28.x) // that can be displayed in not disturb mode if white listed (the later will need compat28.x)
.setCategory(NotificationCompat.CATEGORY_MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE)
// ID of the corresponding shortcut, for conversation features under API 30+ // ID of the corresponding shortcut, for conversation features under API 30+
.setShortcutId(roomInfo.roomId) .setShortcutId(roomInfo.roomId.value)
// Title for API < 16 devices. // Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName) .setContentTitle(roomInfo.roomDisplayName)
// Content for API < 16 devices. // Content for API < 16 devices.
@ -259,7 +262,7 @@ class NotificationUtils @Inject constructor(
) )
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // 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 // devices and all Wear devices. But we want a custom grouping, so we specify the groupID
.setGroup(roomInfo.sessionId) .setGroup(roomInfo.sessionId.value)
// In order to avoid notification making sound twice (due to the summary notification) // In order to avoid notification making sound twice (due to the summary notification)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
@ -359,7 +362,7 @@ class NotificationUtils @Inject constructor(
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
.setContentText(inviteNotifiableEvent.description) .setContentText(inviteNotifiableEvent.description)
.setGroup(inviteNotifiableEvent.sessionId) .setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
.setColor(accentColor) .setColor(accentColor)
@ -446,7 +449,7 @@ class NotificationUtils @Inject constructor(
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName) .setContentTitle(buildMeta.applicationName)
.setContentText(simpleNotifiableEvent.description) .setContentText(simpleNotifiableEvent.description)
.setGroup(simpleNotifiableEvent.sessionId) .setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
.setColor(accentColor) .setColor(accentColor)
@ -477,7 +480,7 @@ class NotificationUtils @Inject constructor(
.build() .build()
} }
private fun buildOpenRoomIntent(sessionId: String, roomId: String): PendingIntent? { private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? {
val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null) val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null)
roomIntent.action = actionIds.tapToView roomIntent.action = actionIds.tapToView
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
@ -491,7 +494,7 @@ class NotificationUtils @Inject constructor(
) )
} }
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? { private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? {
val sessionId = roomInfo.sessionId val sessionId = roomInfo.sessionId
val roomId = roomInfo.roomId val roomId = roomInfo.roomId
val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
@ -507,7 +510,7 @@ class NotificationUtils @Inject constructor(
) )
} }
private fun buildOpenHomePendingIntentForSummary(sessionId: String): PendingIntent { private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent {
val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null) val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null)
intent.data = createIgnoredUri("tapSummary?$sessionId") intent.data = createIgnoredUri("tapSummary?$sessionId")
return PendingIntent.getActivity( return PendingIntent.getActivity(
@ -526,9 +529,9 @@ class NotificationUtils @Inject constructor(
it will be more appropriate to use an activity. Since you have to provide your own UI. it will be more appropriate to use an activity. Since you have to provide your own UI.
*/ */
private fun buildQuickReplyIntent( private fun buildQuickReplyIntent(
sessionId: String, sessionId: SessionId,
roomId: String, roomId: RoomId,
threadId: String?, threadId: ThreadId?,
senderName: String? senderName: String?
): PendingIntent? { ): PendingIntent? {
val intent: Intent val intent: Intent
@ -573,7 +576,7 @@ class NotificationUtils @Inject constructor(
* Build the summary notification. * Build the summary notification.
*/ */
fun buildSummaryListNotification( fun buildSummaryListNotification(
sessionId: String, sessionId: SessionId,
style: NotificationCompat.InboxStyle?, style: NotificationCompat.InboxStyle?,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
@ -587,12 +590,12 @@ class NotificationUtils @Inject constructor(
// used in compat < N, after summary is built based on child notifications // used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp) .setWhen(lastMessageTimestamp)
.setStyle(style) .setStyle(style)
.setContentTitle(sessionId) .setContentTitle(sessionId.value)
.setCategory(NotificationCompat.CATEGORY_MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
// set content text to support devices running API level < 24 // set content text to support devices running API level < 24
.setContentText(compatSummary) .setContentText(compatSummary)
.setGroup(sessionId) .setGroup(sessionId.value)
// set this notification as the summary for the group // set this notification as the summary for the group
.setGroupSummary(true) .setGroupSummary(true)
.setColor(accentColor) .setColor(accentColor)
@ -616,7 +619,7 @@ class NotificationUtils @Inject constructor(
.build() .build()
} }
private fun getDismissSummaryPendingIntent(sessionId: String): PendingIntent { private fun getDismissSummaryPendingIntent(sessionId: SessionId): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java) val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.dismissSummary intent.action = actionIds.dismissSummary
intent.data = createIgnoredUri("deleteSummary?$sessionId") intent.data = createIgnoredUri("deleteSummary?$sessionId")

View file

@ -16,12 +16,15 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
/** /**
* Data class to hold information about a group of notifications for a room. * Data class to hold information about a group of notifications for a room.
*/ */
data class RoomEventGroupInfo( data class RoomEventGroupInfo(
val sessionId: String, val sessionId: SessionId,
val roomId: String, val roomId: RoomId,
val roomDisplayName: String, val roomDisplayName: String,
val isDirect: Boolean = false val isDirect: Boolean = false
) { ) {

View file

@ -19,6 +19,8 @@ package io.element.android.libraries.push.impl.notifications
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
@ -34,9 +36,9 @@ class RoomGroupMessageCreator @Inject constructor(
) { ) {
fun createRoomMessage( fun createRoomMessage(
sessionId: String, sessionId: SessionId,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: String, roomId: RoomId,
userDisplayName: String, userDisplayName: String,
userAvatarUrl: String? userAvatarUrl: String?
): RoomNotification.Message { ): RoomNotification.Message {
@ -47,7 +49,7 @@ class RoomGroupMessageCreator @Inject constructor(
Person.Builder() Person.Builder()
.setName(userDisplayName) .setName(userDisplayName)
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) .setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
.setKey(lastKnownRoomEvent.sessionId) .setKey(lastKnownRoomEvent.sessionId.value)
.build() .build()
).also { ).also {
it.conversationTitle = roomName.takeIf { roomIsGroup } it.conversationTitle = roomName.takeIf { roomIsGroup }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject import javax.inject.Inject
@ -42,7 +43,7 @@ class SummaryGroupMessageCreator @Inject constructor(
) { ) {
fun createSummaryNotification( fun createSummaryNotification(
sessionId: String, sessionId: SessionId,
roomNotifications: List<RoomNotification.Message.Meta>, roomNotifications: List<RoomNotification.Message.Meta>,
invitationNotifications: List<OneShotNotification.Append.Meta>, invitationNotifications: List<OneShotNotification.Append.Meta>,
simpleNotifications: List<OneShotNotification.Append.Meta>, simpleNotifications: List<OneShotNotification.Append.Meta>,
@ -92,7 +93,7 @@ class SummaryGroupMessageCreator @Inject constructor(
} }
private fun processSimpleGroupSummary( private fun processSimpleGroupSummary(
sessionId: String, sessionId: SessionId,
summaryIsNoisy: Boolean, summaryIsNoisy: Boolean,
messageEventsCount: Int, messageEventsCount: Int,
simpleEventsCount: Int, simpleEventsCount: Int,

View file

@ -15,11 +15,15 @@
*/ */
package io.element.android.libraries.push.impl.notifications.model package io.element.android.libraries.push.impl.notifications.model
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
data class InviteNotifiableEvent( data class InviteNotifiableEvent(
override val sessionId: String, override val sessionId: SessionId,
override val roomId: String, override val roomId: RoomId,
override val eventId: String, override val eventId: EventId,
override val editedEventId: String?, override val editedEventId: EventId?,
override val canBeReplaced: Boolean, override val canBeReplaced: Boolean,
val roomName: String?, val roomName: String?,
val noisy: Boolean, val noisy: Boolean,

View file

@ -15,16 +15,19 @@
*/ */
package io.element.android.libraries.push.impl.notifications.model package io.element.android.libraries.push.impl.notifications.model
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 java.io.Serializable import java.io.Serializable
/** /**
* Parent interface for all events which can be displayed as a Notification. * Parent interface for all events which can be displayed as a Notification.
*/ */
sealed interface NotifiableEvent : Serializable { sealed interface NotifiableEvent : Serializable {
val sessionId: String val sessionId: SessionId
val roomId: String val roomId: RoomId
val eventId: String val eventId: EventId
val editedEventId: String? val editedEventId: EventId?
// Used to know if event should be replaced with the one coming from eventstream // Used to know if event should be replaced with the one coming from eventstream
val canBeReplaced: Boolean val canBeReplaced: Boolean

View file

@ -16,16 +16,20 @@
package io.element.android.libraries.push.impl.notifications.model package io.element.android.libraries.push.impl.notifications.model
import android.net.Uri import android.net.Uri
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.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.currentRoomId import io.element.android.services.appnavstate.api.currentRoomId
import io.element.android.services.appnavstate.api.currentSessionId import io.element.android.services.appnavstate.api.currentSessionId
import io.element.android.services.appnavstate.api.currentThreadId import io.element.android.services.appnavstate.api.currentThreadId
data class NotifiableMessageEvent( data class NotifiableMessageEvent(
override val sessionId: String, override val sessionId: SessionId,
override val roomId: String, override val roomId: RoomId,
override val eventId: String, override val eventId: EventId,
override val editedEventId: String?, override val editedEventId: EventId?,
override val canBeReplaced: Boolean, override val canBeReplaced: Boolean,
val noisy: Boolean, val noisy: Boolean,
val timestamp: Long, val timestamp: Long,
@ -35,7 +39,7 @@ data class NotifiableMessageEvent(
// We cannot use Uri? type here, as that could trigger a // We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage // NotSerializableException when persisting this to storage
val imageUriString: String?, val imageUriString: String?,
val threadId: String?, val threadId: ThreadId?,
val roomName: String?, val roomName: String?,
val roomIsDirect: Boolean = false, val roomIsDirect: Boolean = false,
val roomAvatarPath: String? = null, val roomAvatarPath: String? = null,
@ -59,9 +63,9 @@ data class NotifiableMessageEvent(
fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom( fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(
appNavigationState: AppNavigationState? appNavigationState: AppNavigationState?
): Boolean { ): Boolean {
val currentSessionId = appNavigationState?.currentSessionId()?.value ?: return false val currentSessionId = appNavigationState?.currentSessionId() ?: return false
return when (val currentRoomId = appNavigationState.currentRoomId()?.value) { return when (val currentRoomId = appNavigationState.currentRoomId()) {
null -> false null -> false
else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()?.value else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()
} }
} }

View file

@ -15,11 +15,15 @@
*/ */
package io.element.android.libraries.push.impl.notifications.model package io.element.android.libraries.push.impl.notifications.model
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
data class SimpleNotifiableEvent( data class SimpleNotifiableEvent(
override val sessionId: String, override val sessionId: SessionId,
override val roomId: String, override val roomId: RoomId,
override val eventId: String, override val eventId: EventId,
override val editedEventId: String?, override val editedEventId: EventId?,
val noisy: Boolean, val noisy: Boolean,
val title: String, val title: String,
val description: String, val description: String,

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2022 New Vector Ltd * Copyright (c) 2023 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
package io.element.android.libraries.push.impl.push package io.element.android.libraries.push.impl.push
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
/** /**
* Represent parsed data that the app has received from a Push content. * Represent parsed data that the app has received from a Push content.
* *
@ -24,8 +27,8 @@ package io.element.android.libraries.push.impl.push
* @property unread Number of unread message. * @property unread Number of unread message.
*/ */
data class PushData( data class PushData(
val eventId: String?, val eventId: EventId?,
val roomId: String?, val roomId: RoomId?,
val unread: Int?, val unread: Int?,
val clientSecret: String?, val clientSecret: String?,
) )

View file

@ -119,7 +119,7 @@ class PushHandler @Inject constructor(
// Get userId from client secret // Get userId from client secret
pushClientSecret.getUserIdFromSecret(clientSecret) pushClientSecret.getUserIdFromSecret(clientSecret)
} ?: run { } ?: run {
matrixAuthenticationService.getLatestSessionId()?.value matrixAuthenticationService.getLatestSessionId()
} }
if (userId == null) { if (userId == null) {

View file

@ -15,6 +15,7 @@
*/ */
package io.element.android.libraries.push.impl.pushgateway package io.element.android.libraries.push.impl.pushgateway
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.api.gateway.PushGatewayFailure import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject import javax.inject.Inject
@ -26,7 +27,7 @@ class PushGatewayNotifyRequest @Inject constructor(
val url: String, val url: String,
val appId: String, val appId: String,
val pushKey: String, val pushKey: String,
val eventId: String val eventId: EventId
) )
suspend fun execute(params: Params) { suspend fun execute(params: Params) {
@ -38,7 +39,7 @@ class PushGatewayNotifyRequest @Inject constructor(
val response = sygnalApi.notify( val response = sygnalApi.notify(
PushGatewayNotifyBody( PushGatewayNotifyBody(
PushGatewayNotification( PushGatewayNotification(
eventId = params.eventId, eventId = params.eventId.value,
devices = listOf( devices = listOf(
PushGatewayDevice( PushGatewayDevice(
params.appId, params.appId,

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.push.impl.unifiedpush package io.element.android.libraries.push.impl.unifiedpush
import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.push.impl.push.PushData import io.element.android.libraries.push.impl.push.PushData
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -55,8 +57,8 @@ data class PushDataUnifiedPushCounts(
) )
fun PushDataUnifiedPush.toPushData() = PushData( fun PushDataUnifiedPush.toPushData() = PushData(
eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
unread = notification?.counts?.unread, unread = notification?.counts?.unread,
clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush
) )

View file

@ -16,24 +16,26 @@
package io.element.android.libraries.push.impl.clientsecret package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
class InMemoryPushClientSecretStore : PushClientSecretStore { class InMemoryPushClientSecretStore : PushClientSecretStore {
private val secrets = mutableMapOf<String, String>() private val secrets = mutableMapOf<SessionId, String>()
fun getSecrets(): Map<String, String> = secrets fun getSecrets(): Map<SessionId, String> = secrets
override suspend fun storeSecret(userId: String, clientSecret: String) { override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
secrets[userId] = clientSecret secrets[userId] = clientSecret
} }
override suspend fun getSecret(userId: String): String? { override suspend fun getSecret(userId: SessionId): String? {
return secrets[userId] return secrets[userId]
} }
override suspend fun resetSecret(userId: String) { override suspend fun resetSecret(userId: SessionId) {
secrets.remove(userId) secrets.remove(userId)
} }
override suspend fun getUserIdFromSecret(clientSecret: String): String? { override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return secrets.keys.firstOrNull { secrets[it] == clientSecret } return secrets.keys.firstOrNull { secrets[it] == clientSecret }
} }
} }

View file

@ -19,12 +19,13 @@
package io.element.android.libraries.push.impl.clientsecret package io.element.android.libraries.push.impl.clientsecret
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
private const val A_USER_ID_0 = "A_USER_ID_0" private val A_USER_ID_0 = SessionId("A_USER_ID_0")
private const val A_USER_ID_1 = "A_USER_ID_1" private val A_USER_ID_1 = SessionId("A_USER_ID_1")
private const val A_UNKNOWN_SECRET = "A_UNKNOWN_SECRET" private const val A_UNKNOWN_SECRET = "A_UNKNOWN_SECRET"

View file

@ -0,0 +1,194 @@
/*
* 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.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.FakeOutdatedEventDetector
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.test.anAppNavigationState
import org.junit.Test
private val NOT_VIEWING_A_ROOM = anAppNavigationState()
private val VIEWING_A_ROOM = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
private val VIEWING_A_THREAD = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
class NotifiableEventProcessorTest {
private val outdatedDetector = FakeOutdatedEventDetector()
private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance)
@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 result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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 = "m.room.redaction"))
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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 result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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))
outdatedDetector.givenEventIsOutOfDate(events[0])
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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))
val result = eventProcessor.process(events, VIEWING_A_ROOM, 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))
val result = eventProcessor.process(events, VIEWING_A_THREAD, 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))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, VIEWING_A_ROOM, 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))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, VIEWING_A_THREAD, 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 result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, 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)
}
}

View file

@ -0,0 +1,231 @@
/*
* 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)
}

View file

@ -0,0 +1,194 @@
/*
* 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.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.FakeNotificationUtils
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 org.junit.Test
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)
class NotificationFactoryTest {
private val notificationUtils = FakeNotificationUtils()
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
private val notificationFactory = NotificationFactory(
notificationUtils.instance,
roomGroupMessageCreator.instance,
summaryGroupMessageCreator.instance
)
@Test
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(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 = notificationUtils.givenBuildSimpleInvitationNotificationFor(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 = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL
)
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))
val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
assertThat(result).isEqualTo(listOf(expectedNotification))
}
@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 result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
assertThat(result).isEqualTo(
listOf(
RoomNotification.Removed(
roomId = A_ROOM_ID
)
)
)
}
@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 result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
assertThat(result).isEqualTo(
listOf(
RoomNotification.Removed(
roomId = A_ROOM_ID
)
)
)
}
@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 = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID,
withRedactedRemoved,
A_ROOM_ID,
A_SESSION_ID.value,
MY_AVATAR_URL
)
val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
assertThat(result).isEqualTo(listOf(expectedNotification))
}
}
fun <T> testWith(receiver: T, block: T.() -> Unit) {
receiver.block()
}

View file

@ -0,0 +1,227 @@
/*
* 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 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.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.mockk
import org.junit.Test
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())
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)
class NotificationRendererTest {
private val notificationDisplayer = FakeNotificationDisplayer()
private val notificationFactory = FakeNotificationFactory()
private val notificationIdProvider = NotificationIdProvider()
private val notificationRenderer = NotificationRenderer(
notificationIdProvider = notificationIdProvider,
notificationDisplayer = notificationDisplayer.instance,
notificationFactory = notificationFactory.instance,
)
@Test
fun `given no notifications when rendering then cancels summary notification`() {
givenNoNotifications()
renderEventsAsNotifications()
notificationDisplayer.verifySummaryCancelled()
notificationDisplayer.verifyNoOtherInteractions()
}
@Test
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given a room message group notification is added when rendering then show the message notification and update summary`() {
givenNotifications(
roomNotifications = listOf(
RoomNotification.Message(
A_NOTIFICATION,
MESSAGE_META
)
)
)
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)))
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
A_NOTIFICATION,
ONE_SHOT_META.copy(key = AN_EVENT_ID.value)
)
)
)
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)))
renderEventsAsNotifications()
notificationDisplayer.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`() {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
A_NOTIFICATION,
ONE_SHOT_META.copy(key = A_ROOM_ID.value)
)
)
)
renderEventsAsNotifications()
notificationDisplayer.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 fun renderEventsAsNotifications() {
notificationRenderer.render(
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST
)
}
private fun givenNoNotifications() {
givenNotifications(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(),
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION
) {
notificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS,
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = useCompleteNotificationFormat,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
summaryNotification = summaryNotification
)
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.mockk
import io.mockk.verify
import io.mockk.verifyOrder
class FakeNotificationDisplayer {
val instance = mockk<NotificationDisplayer>(relaxed = true)
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()
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.core.SessionId
import io.element.android.libraries.push.impl.notifications.*
import io.mockk.every
import io.mockk.mockk
class FakeNotificationFactory {
val instance = mockk<NotificationFactory>()
fun givenNotificationsFor(
groupedEvents: GroupedNotificationEvents,
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
summaryNotification: SummaryNotification
) {
with(instance) {
every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
every {
createSummaryNotification(
sessionId,
roomNotifications,
invitationNotifications,
simpleNotifications,
useCompleteNotificationFormat
)
} returns summaryNotification
}
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.NotificationUtils
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 FakeNotificationUtils {
val instance = mockk<NotificationUtils>()
fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
val mockNotification = mockk<Notification>()
every { instance.buildRoomInvitationNotification(event) } returns mockNotification
return mockNotification
}
fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification {
val mockNotification = mockk<Notification>()
every { instance.buildSimpleEventNotification(event) } returns mockNotification
return mockNotification
}
}

View file

@ -0,0 +1,34 @@
/*
* 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 FakeOutdatedEventDetector {
val instance = mockk<OutdatedEventDetector>()
fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) {
every { instance.isMessageOutdated(notifiableEvent) } returns true
}
fun givenEventIsInDate(notifiableEvent: NotifiableEvent) {
every { instance.isMessageOutdated(notifiableEvent) } returns false
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.mockk.every
import io.mockk.mockk
class FakeRoomGroupMessageCreator {
val instance = mockk<RoomGroupMessageCreator>()
fun givenCreatesRoomMessageFor(
sessionId: SessionId,
events: List<NotifiableMessageEvent>,
roomId: RoomId,
userDisplayName: String,
userAvatarUrl: String?
): RoomNotification.Message {
val mockMessage = mockk<RoomNotification.Message>()
every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage
return mockMessage
}
}

View file

@ -0,0 +1,25 @@
/*
* 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.SummaryGroupMessageCreator
import io.mockk.mockk
class FakeSummaryGroupMessageCreator {
val instance = mockk<SummaryGroupMessageCreator>()
}

View file

@ -0,0 +1,96 @@
/*
* 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.fixtures
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.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.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
fun aSimpleNotifiableEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
type: String? = null,
isRedacted: Boolean = false,
canBeReplaced: Boolean = false,
editedEventId: EventId? = null
) = SimpleNotifiableEvent(
sessionId = sessionId,
roomId = roomId,
eventId = eventId,
editedEventId = editedEventId,
noisy = false,
title = "title",
description = "description",
type = type,
timestamp = 0,
soundName = null,
canBeReplaced = canBeReplaced,
isRedacted = isRedacted
)
fun anInviteNotifiableEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
isRedacted: Boolean = false
) = InviteNotifiableEvent(
sessionId = sessionId,
eventId = eventId,
roomId = roomId,
roomName = "a room name",
editedEventId = null,
noisy = false,
title = "title",
description = "description",
type = null,
timestamp = 0,
soundName = null,
canBeReplaced = false,
isRedacted = isRedacted
)
fun aNotifiableMessageEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
threadId: ThreadId? = null,
isRedacted: Boolean = false
) = NotifiableMessageEvent(
sessionId = sessionId,
eventId = eventId,
editedEventId = null,
noisy = false,
timestamp = 0,
senderName = "sender-name",
senderId = "sending-id",
body = "message-body",
roomId = roomId,
threadId = threadId,
roomName = "room-name",
roomIsDirect = false,
canBeReplaced = false,
isRedacted = isRedacted,
imageUriString = null
)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.services.appnavstate.test"
}
dependencies {
api(projects.libraries.matrix.api)
api(projects.services.appnavstate.api)
}

View file

@ -0,0 +1,44 @@
/*
* 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.services.appnavstate.test
import io.element.android.libraries.matrix.api.core.*
import io.element.android.services.appnavstate.api.AppNavigationState
fun anAppNavigationState(
sessionId: SessionId? = null,
spaceId: SpaceId? = MAIN_SPACE,
roomId: RoomId? = null,
threadId: ThreadId? = null,
): AppNavigationState {
if (sessionId == null) {
return AppNavigationState.Root
}
val session = AppNavigationState.Session(sessionId)
if (spaceId == null) {
return session
}
val space = AppNavigationState.Space(spaceId, session)
if (roomId == null) {
return space
}
val room = AppNavigationState.Room(roomId, space)
if (threadId == null) {
return room
}
return AppNavigationState.Thread(threadId, room)
}