Merge branch 'develop' into feature/fga/space_flow_inject_room

This commit is contained in:
ganfra
2025-10-27 11:41:26 +01:00
75 changed files with 561 additions and 337 deletions

View File

@@ -7,8 +7,8 @@
package io.element.android.appconfig
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
object NotificationConfig {
/**
@@ -27,5 +27,5 @@ object NotificationConfig {
const val SHOW_QUICK_REPLY_ACTION = true
@ColorInt
val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
val NOTIFICATION_ACCENT_COLOR: Int = "#FF0DBD8B".toColorInt()
}

View File

@@ -7,6 +7,7 @@
package io.element.android.features.enterprise.api
import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
@@ -24,6 +25,8 @@ interface EnterpriseService {
*/
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
fun brandColorsFlow(sessionId: SessionId?): Flow<Color?>
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
fun firebasePushGateway(): String?

View File

@@ -7,6 +7,7 @@
package io.element.android.features.enterprise.impl
import androidx.compose.ui.graphics.Color
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.compound.colors.SemanticColorsLightDark
@@ -27,6 +28,10 @@ class DefaultEnterpriseService : EnterpriseService {
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
override fun brandColorsFlow(sessionId: SessionId?): Flow<Color?> {
return flowOf(null)
}
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
return flowOf(SemanticColorsLightDark.default)
}

View File

@@ -51,6 +51,16 @@ class DefaultEnterpriseServiceTest {
}
}
@Test
fun `brandColorsFlow always emits null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
defaultEnterpriseService.brandColorsFlow(null).test {
val initialState = awaitItem()
assertThat(initialState).isNull()
awaitComplete()
}
}
@Test
fun `semanticColorsFlow always emits the same value for a session`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()

View File

@@ -7,6 +7,7 @@
package io.element.android.features.enterprise.test
import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
@@ -27,6 +28,7 @@ class FakeEnterpriseService(
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow<Color?>(null)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
@@ -45,6 +47,10 @@ class FakeEnterpriseService(
overrideBrandColorResult(sessionId, brandColor)
}
override fun brandColorsFlow(sessionId: SessionId?): Flow<Color?> {
return brandColorState.asStateFlow()
}
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
return semanticColorsState.asStateFlow()
}

View File

@@ -9,6 +9,7 @@ package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -57,20 +58,24 @@ fun HomeSpacesView(
item {
HorizontalDivider()
}
state.spaceRooms.forEach { spaceRoom ->
item(spaceRoom.roomId) {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onSpaceClick(spaceRoom.roomId)
},
onLongClick = {
// TODO
},
)
itemsIndexed(
items = state.spaceRooms,
key = { _, spaceRoom -> spaceRoom.roomId }
) { index, spaceRoom ->
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onSpaceClick(spaceRoom.roomId)
},
onLongClick = {
// TODO
},
)
if (index != state.spaceRooms.lastIndex) {
HorizontalDivider()
}
}
}

View File

@@ -7,6 +7,7 @@
package io.element.android.features.space.impl.root
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
@@ -186,32 +188,36 @@ private fun SpaceViewContent(
HorizontalDivider()
}
}
state.children.forEach { spaceRoom ->
item {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onRoomClick(spaceRoom)
itemsIndexed(
items = state.children,
key = { _, spaceRoom -> spaceRoom.roomId }
) { index, spaceRoom ->
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onRoomClick(spaceRoom)
},
onLongClick = {
// TODO
},
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
},
bottomAction = spaceRoom.inviteButtons(
onAcceptClick = {
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
},
onLongClick = {
// TODO
},
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
},
bottomAction = spaceRoom.inviteButtons(
onAcceptClick = {
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
},
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
}
)
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
}
)
)
if (index != state.children.lastIndex) {
HorizontalDivider()
}
}
if (state.hasMoreToLoad) {
@@ -266,7 +272,7 @@ private fun SpaceViewTopBar(
modifier = Modifier
.clip(roundedCornerShape)
// TODO enable when screen ready for space
// .clickable(onClick = onDetailsClick)
.clickable(enabled = false, onClick = onDetailsClick)
)
}
},
@@ -338,10 +344,10 @@ private fun SpaceAvatarAndNameRow(
)
Text(
modifier = Modifier
.padding(horizontal = 8.dp)
.semantics {
heading()
},
.padding(horizontal = 8.dp)
.semantics {
heading()
},
text = name ?: stringResource(CommonStrings.common_no_space_name),
style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { name == null },

View File

@@ -16,7 +16,7 @@ import kotlinx.serialization.json.Json
/**
* Provides a Json instance configured to ignore unknown keys.
*/
interface JsonProvider : Provider<Json>
fun interface JsonProvider : Provider<Json>
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)

View File

@@ -98,3 +98,5 @@ const val A_TIMESTAMP = 567L
const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
const val A_LOGIN_HINT = "mxid:@alice:example.org"
const val A_COLOR_INT = 0xFF0000

View File

@@ -43,7 +43,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.unreadIndicator
@@ -81,56 +80,50 @@ fun SpaceRoomItemView(
interactionSource = remember { MutableInteractionSource() }
)
.onKeyboardContextMenuAction { onLongClick }
Box(modifier = modifier.then(clickModifier)) {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
Column(
modifier = modifier
.then(clickModifier)
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
SpaceRoomItemScaffold(
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
isSpace = spaceRoom.isSpace,
hideAvatars = hideAvatars,
heroes = spaceRoom.heroes
.map { hero -> hero.getAvatarData(AvatarSize.SpaceListItem) }
.toImmutableList(),
trailingAction = trailingAction,
) {
SpaceRoomItemScaffold(
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
isSpace = spaceRoom.isSpace,
hideAvatars = hideAvatars,
heroes = spaceRoom.heroes
.map { hero -> hero.getAvatarData(AvatarSize.SpaceListItem) }
.toImmutableList(),
trailingAction = trailingAction,
) {
NameAndIndicatorRow(
name = spaceRoom.displayName,
showIndicator = showUnreadIndicator
NameAndIndicatorRow(
name = spaceRoom.displayName,
showIndicator = showUnreadIndicator
)
Spacer(modifier = Modifier.height(1.dp))
SubtitleRow(
visibilityIcon = spaceRoom.visibilityIcon(),
subtitle = spaceRoom.subtitle()
)
Spacer(modifier = Modifier.height(1.dp))
val info = spaceRoom.info()
if (info.isNotBlank()) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = info,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(1.dp))
SubtitleRow(
visibilityIcon = spaceRoom.visibilityIcon(),
subtitle = spaceRoom.subtitle()
)
Spacer(modifier = Modifier.height(1.dp))
val info = spaceRoom.info()
if (info.isNotBlank()) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = info,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp))
// Match the padding of the text content (avatar + spacer)
Box(modifier = Modifier.padding(start = AvatarSize.SpaceListItem.dp + 16.dp)) {
bottomAction()
}
Spacer(modifier = Modifier.height(4.dp))
}
}
HorizontalDivider(
modifier = Modifier
// Match the padding of the text content (padding + avatar + spacer)
.padding(start = AvatarSize.SpaceListItem.dp + 16.dp + 16.dp)
.align(Alignment.BottomCenter)
)
if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp))
// Match the padding of the text content (avatar + spacer)
Box(modifier = Modifier.padding(start = AvatarSize.SpaceListItem.dp + 16.dp)) {
bottomAction()
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@@ -264,7 +257,6 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class
hideAvatars = false,
onClick = {},
onLongClick = {},
modifier = Modifier.fillMaxWidth().padding(8.dp),
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
{ InviteButtonsRowMolecule({}, {}) }
} else {

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.workmanager.api)
implementation(projects.features.call.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.lockscreen.api)
implementation(projects.libraries.featureflag.api)
api(projects.libraries.pushproviders.api)
@@ -77,6 +78,7 @@ dependencies {
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.libraries.workmanager.test)
testImplementation(projects.features.call.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.services.appnavstate.test)

View File

@@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import android.graphics.Typeface
import android.text.style.StyleSpan
import androidx.annotation.ColorInt
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import coil3.ImageLoader
@@ -31,17 +32,29 @@ interface NotificationDataFactory {
messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader,
@ColorInt color: Int,
): List<RoomNotification>
@JvmName("toNotificationInvites")
@Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification>
fun toNotifications(
invites: List<InviteNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification>
@JvmName("toNotificationSimpleEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification>
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
fun toNotifications(
fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification>
fun createSummaryNotification(
currentUser: MatrixUser,
@@ -49,6 +62,7 @@ interface NotificationDataFactory {
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): SummaryNotification
}
@@ -64,6 +78,7 @@ class DefaultNotificationDataFactory(
messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader,
@ColorInt color: Int,
): List<RoomNotification> {
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
.groupBy { it.roomId }
@@ -76,6 +91,7 @@ class DefaultNotificationDataFactory(
roomId = roomId,
imageLoader = imageLoader,
existingNotification = getExistingNotificationForMessages(currentUser.userId, roomId),
color = color,
)
RoomNotification(
notification = notification,
@@ -96,11 +112,14 @@ class DefaultNotificationDataFactory(
@JvmName("toNotificationInvites")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
invites: List<InviteNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return invites.map { event ->
OneShotNotification(
key = event.roomId.value,
notification = notificationCreator.createRoomInvitationNotification(event),
notification = notificationCreator.createRoomInvitationNotification(event, color),
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
@@ -110,11 +129,14 @@ class DefaultNotificationDataFactory(
@JvmName("toNotificationSimpleEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return simpleEvents.map { event ->
OneShotNotification(
key = event.eventId.value,
notification = notificationCreator.createSimpleEventNotification(event),
notification = notificationCreator.createSimpleEventNotification(event, color),
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
@@ -124,11 +146,14 @@ class DefaultNotificationDataFactory(
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return fallback.map { event ->
OneShotNotification(
key = event.eventId.value,
notification = notificationCreator.createFallbackNotification(event),
notification = notificationCreator.createFallbackNotification(event, color),
summaryLine = event.description.orEmpty(),
isNoisy = false,
timestamp = event.timestamp
@@ -142,6 +167,7 @@ class DefaultNotificationDataFactory(
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): SummaryNotification {
return when {
roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty() -> SummaryNotification.Removed
@@ -152,6 +178,7 @@ class DefaultNotificationDataFactory(
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
color = color,
)
)
}

View File

@@ -7,8 +7,11 @@
package io.element.android.libraries.push.impl.notifications
import androidx.compose.ui.graphics.toArgb
import coil3.ImageLoader
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@@ -18,6 +21,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import kotlinx.coroutines.flow.first
import timber.log.Timber
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
@@ -26,6 +30,7 @@ private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.Notification
class NotificationRenderer(
private val notificationDisplayer: NotificationDisplayer,
private val notificationDataFactory: NotificationDataFactory,
private val enterpriseService: EnterpriseService,
) {
suspend fun render(
currentUser: MatrixUser,
@@ -33,17 +38,20 @@ class NotificationRenderer(
eventsToProcess: List<NotifiableEvent>,
imageLoader: ImageLoader,
) {
val color = enterpriseService.brandColorsFlow(currentUser.userId).first()?.toArgb()
?: NotificationConfig.NOTIFICATION_ACCENT_COLOR
val groupedEvents = eventsToProcess.groupByType()
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents)
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents)
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents)
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader, color)
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, color)
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, color)
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, color)
val summaryNotification = notificationDataFactory.createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
color = color,
)
// Remove summary first to avoid briefly displaying it after dismissing the last notification

View File

@@ -10,13 +10,13 @@ package io.element.android.libraries.push.impl.notifications
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.push.api.push.NotificationEventRequest
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -48,7 +48,7 @@ class DefaultNotificationResolverQueue(
private val appCoroutineScope: CoroutineScope,
private val workManagerScheduler: WorkManagerScheduler,
private val featureFlagService: FeatureFlagService,
private val json: JsonProvider,
private val workerDataConverter: WorkerDataConverter,
) : NotificationResolverQueue {
companion object {
private const val BATCH_WINDOW_MS = 250L
@@ -99,7 +99,7 @@ class DefaultNotificationResolverQueue(
SyncNotificationWorkManagerRequest(
sessionId = sessionId,
notificationEventRequests = requests,
json = json,
workerDataConverter = workerDataConverter,
)
)
}

View File

@@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import android.graphics.Bitmap
import androidx.annotation.ColorInt
import coil3.ImageLoader
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
@@ -28,6 +29,7 @@ interface RoomGroupMessageCreator {
roomId: RoomId,
imageLoader: ImageLoader,
existingNotification: Notification?,
@ColorInt color: Int,
): Notification
}
@@ -43,6 +45,7 @@ class DefaultRoomGroupMessageCreator(
roomId: RoomId,
imageLoader: ImageLoader,
existingNotification: Notification?,
@ColorInt color: Int,
): Notification {
val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)"
@@ -60,24 +63,25 @@ class DefaultRoomGroupMessageCreator(
val smartReplyErrors = events.filter { it.isSmartReplyError() }
val roomIsDm = !roomIsGroup
return notificationCreator.createMessagesListNotification(
RoomEventGroupInfo(
sessionId = currentUser.userId,
roomId = roomId,
roomDisplayName = roomName,
isDm = roomIsDm,
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
shouldBing = events.any { it.noisy },
customSound = events.last().soundName,
isUpdated = events.last().isUpdated,
),
threadId = lastKnownRoomEvent.threadId,
largeIcon = largeBitmap,
lastMessageTimestamp = lastMessageTimestamp,
tickerText = tickerText,
currentUser = currentUser,
existingNotification = existingNotification,
imageLoader = imageLoader,
events = events,
RoomEventGroupInfo(
sessionId = currentUser.userId,
roomId = roomId,
roomDisplayName = roomName,
isDm = roomIsDm,
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
shouldBing = events.any { it.noisy },
customSound = events.last().soundName,
isUpdated = events.last().isUpdated,
),
threadId = lastKnownRoomEvent.threadId,
largeIcon = largeBitmap,
lastMessageTimestamp = lastMessageTimestamp,
tickerText = tickerText,
currentUser = currentUser,
existingNotification = existingNotification,
imageLoader = imageLoader,
events = events,
color = color,
)
}

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import androidx.annotation.ColorInt
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -22,6 +23,7 @@ interface SummaryGroupMessageCreator {
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): Notification
}
@@ -45,6 +47,7 @@ class DefaultSummaryGroupMessageCreator(
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): Notification {
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
invitationNotifications.any { it.isNoisy } ||
@@ -61,7 +64,8 @@ class DefaultSummaryGroupMessageCreator(
currentUser,
sumTitle,
noisy = summaryIsNoisy,
lastMessageTimestamp = lastMessageTimestamp
lastMessageTimestamp = lastMessageTimestamp,
color = color,
)
}
}

View File

@@ -10,16 +10,13 @@ package io.element.android.libraries.push.impl.notifications.factories
import android.app.Notification
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import androidx.annotation.DrawableRes
import androidx.annotation.ColorInt
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person
import androidx.core.content.res.ResourcesCompat
import coil3.ImageLoader
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.di.annotations.ApplicationContext
@@ -57,18 +54,22 @@ interface NotificationCreator {
existingNotification: Notification?,
imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification
fun createRoomInvitationNotification(
inviteNotifiableEvent: InviteNotifiableEvent
inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification
fun createSimpleEventNotification(
simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification
fun createFallbackNotification(
fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification
/**
@@ -78,10 +79,13 @@ interface NotificationCreator {
currentUser: MatrixUser,
compatSummary: String,
noisy: Boolean,
lastMessageTimestamp: Long
lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification
fun createDiagnosticNotification(): Notification
fun createDiagnosticNotification(
@ColorInt color: Int,
): Notification
}
@ContributesBinding(AppScope::class)
@@ -97,8 +101,6 @@ class DefaultNotificationCreator(
private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
private val rejectInvitationActionFactory: RejectInvitationActionFactory
) : NotificationCreator {
private val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR
/**
* Create a notification for a Room.
*/
@@ -112,15 +114,14 @@ class DefaultNotificationCreator(
existingNotification: Notification?,
imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification {
// Build the pending intent for when the notification is clicked
val openIntent = when {
threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId)
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)
}
val smallIcon = CommonDrawables.ic_notification
val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION }
val channelId = if (containsMissedCall) {
notificationChannels.getChannelForIncomingCall(false)
@@ -176,7 +177,7 @@ class DefaultNotificationCreator(
)
.setSmallIcon(smallIcon)
// Set primary color (important for Wear 2.0 Notifications).
.setColor(accentColor)
.setColor(color)
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
// 'importance' which is set in the NotificationChannel. The integers representing
// 'priority' are different from 'importance', so make sure you don't mix them.
@@ -189,7 +190,7 @@ class DefaultNotificationCreator(
setSound(it)
}
*/
setLights(accentColor, 500, 500)
setLights(color, 500, 500)
} else {
priority = NotificationCompat.PRIORITY_LOW
}
@@ -221,7 +222,8 @@ class DefaultNotificationCreator(
}
override fun createRoomInvitationNotification(
inviteNotifiableEvent: InviteNotifiableEvent
inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
@@ -232,7 +234,7 @@ class DefaultNotificationCreator(
.setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
.setColor(color)
.apply {
addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
@@ -247,7 +249,7 @@ class DefaultNotificationCreator(
setSound(it)
}
*/
setLights(accentColor, 500, 500)
setLights(color, 500, 500)
} else {
priority = NotificationCompat.PRIORITY_LOW
}
@@ -264,9 +266,9 @@ class DefaultNotificationCreator(
override fun createSimpleEventNotification(
simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
@@ -275,7 +277,7 @@ class DefaultNotificationCreator(
.setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
.setColor(color)
.setAutoCancel(true)
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId))
.apply {
@@ -287,7 +289,7 @@ class DefaultNotificationCreator(
setSound(it)
}
*/
setLights(accentColor, 500, 500)
setLights(color, 500, 500)
} else {
priority = NotificationCompat.PRIORITY_LOW
}
@@ -297,9 +299,9 @@ class DefaultNotificationCreator(
override fun createFallbackNotification(
fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(false)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
@@ -308,7 +310,7 @@ class DefaultNotificationCreator(
.setGroup(fallbackNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
.setColor(color)
.setAutoCancel(true)
.setWhen(fallbackNotifiableEvent.timestamp)
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
@@ -332,7 +334,8 @@ class DefaultNotificationCreator(
currentUser: MatrixUser,
compatSummary: String,
noisy: Boolean,
lastMessageTimestamp: Long
lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(noisy)
@@ -345,7 +348,7 @@ class DefaultNotificationCreator(
.setGroup(currentUser.userId.value)
// set this notification as the summary for the group
.setGroupSummary(true)
.setColor(accentColor)
.setColor(color)
.apply {
if (noisy) {
// Compat
@@ -355,7 +358,7 @@ class DefaultNotificationCreator(
setSound(it)
}
*/
setLights(accentColor, 500, 500)
setLights(color, 500, 500)
} else {
// compat
priority = NotificationCompat.PRIORITY_LOW
@@ -366,14 +369,15 @@ class DefaultNotificationCreator(
.build()
}
override fun createDiagnosticNotification(): Notification {
override fun createDiagnosticNotification(
@ColorInt color: Int,
): Notification {
val intent = pendingIntentFactory.createTestPendingIntent()
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
.setContentTitle(buildMeta.applicationName)
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
.setSmallIcon(CommonDrawables.ic_notification)
.setLargeIcon(getBitmap(R.drawable.element_logo_green))
.setColor(accentColor)
.setColor(color)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
@@ -461,16 +465,6 @@ class DefaultNotificationCreator(
}
}
private fun getBitmap(@DrawableRes drawableRes: Int): Bitmap? {
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null
val canvas = Canvas()
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
canvas.setBitmap(bitmap)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(canvas)
return bitmap
}
companion object {
const val MESSAGE_EVENT_ID = "message_event_id"
}

View File

@@ -14,8 +14,8 @@ import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.androidutils.uri.createIgnoredUri
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@@ -46,7 +46,7 @@ class AcceptInvitationActionFactory(
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Action.Builder(
R.drawable.vector_notification_accept_invitation,
CompoundDrawables.ic_compound_check,
stringProvider.getString(CommonStrings.action_accept),
pendingIntent
).build()

View File

@@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.androidutils.uri.createIgnoredUri
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
@@ -46,7 +47,7 @@ class MarkAsReadActionFactory(
)
return NotificationCompat.Action.Builder(
R.drawable.ic_material_done_all_white,
CompoundDrawables.ic_compound_mark_as_read,
stringProvider.getString(R.string.notification_room_action_mark_as_read),
pendingIntent
)

View File

@@ -16,6 +16,7 @@ import androidx.core.app.RemoteInput
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.androidutils.uri.createIgnoredUri
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -45,7 +46,7 @@ class QuickReplyActionFactory(
.build()
return NotificationCompat.Action.Builder(
R.drawable.vector_notification_quick_reply,
CompoundDrawables.ic_compound_reply,
stringProvider.getString(R.string.notification_room_action_quick_reply),
replyPendingIntent
)

View File

@@ -14,8 +14,8 @@ import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.androidutils.uri.createIgnoredUri
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@@ -46,7 +46,7 @@ class RejectInvitationActionFactory(
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Action.Builder(
R.drawable.vector_notification_reject_invitation,
CompoundDrawables.ic_compound_close,
stringProvider.getString(CommonStrings.action_reject),
pendingIntent
).build()

View File

@@ -7,9 +7,13 @@
package io.element.android.libraries.push.impl.troubleshoot
import dev.zacsweers.metro.AppScope
import androidx.compose.ui.graphics.toArgb
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.NotificationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
@@ -25,13 +29,15 @@ import kotlinx.coroutines.withTimeout
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ContributesIntoSet(AppScope::class)
@ContributesIntoSet(SessionScope::class)
@Inject
class NotificationTest(
private val sessionId: SessionId,
private val notificationCreator: NotificationCreator,
private val notificationDisplayer: NotificationDisplayer,
private val notificationClickHandler: NotificationClickHandler,
private val stringProvider: StringProvider,
private val enterpriseService: EnterpriseService,
) : NotificationTroubleshootTest {
override val order = 50
private val delegate = NotificationTroubleshootTestDelegate(
@@ -43,7 +49,9 @@ class NotificationTest(
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val notification = notificationCreator.createDiagnosticNotification()
val color = enterpriseService.brandColorsFlow(sessionId).first()?.toArgb()
?: NotificationConfig.NOTIFICATION_ACCENT_COLOR
val notification = notificationCreator.createDiagnosticNotification(color)
val result = notificationDisplayer.displayDiagnosticNotification(notification)
if (result) {
coroutineScope.listenToNotificationClick()

View File

@@ -18,7 +18,6 @@ import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.binding
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.ApplicationContext
@@ -47,20 +46,11 @@ class FetchNotificationsWorker(
private val workManagerScheduler: WorkManagerScheduler,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
private val coroutineDispatchers: CoroutineDispatchers,
private val json: JsonProvider,
private val workerDataConverter: WorkerDataConverter,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
Timber.d("FetchNotificationsWorker started")
val rawRequestsJson = inputData.getString("requests") ?: return@withContext Result.failure()
val requests = runCatchingExceptions {
json().decodeFromString<List<SyncNotificationWorkManagerRequest.Data>>(rawRequestsJson).map { it.toRequest() }
}.getOrElse {
Timber.e(it, "Failed to deserialize notification requests")
return@withContext Result.failure()
}
Timber.d("Deserialized ${requests.size} requests")
val requests = workerDataConverter.deserialize(inputData) ?: return@withContext Result.failure()
// Wait for network to be available, but not more than 10 seconds
val hasNetwork = withTimeoutOrNull(10.seconds) {
networkMonitor.connectivity.first { it == NetworkStatus.Connected }
@@ -97,7 +87,7 @@ class FetchNotificationsWorker(
SyncNotificationWorkManagerRequest(
sessionId = failedSessionId,
notificationEventRequests = requestsToRetry,
json = json,
workerDataConverter = workerDataConverter,
)
)
}

View File

@@ -10,11 +10,6 @@ package io.element.android.libraries.push.impl.workmanager
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkRequest
import androidx.work.workDataOf
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
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.push.api.push.NotificationEventRequest
import io.element.android.libraries.workmanager.api.WorkManagerRequest
@@ -28,24 +23,19 @@ import java.security.InvalidParameterException
class SyncNotificationWorkManagerRequest(
private val sessionId: SessionId,
private val notificationEventRequests: List<NotificationEventRequest>,
private val json: JsonProvider,
private val workerDataConverter: WorkerDataConverter,
) : WorkManagerRequest {
override fun build(): Result<WorkRequest> {
if (notificationEventRequests.isEmpty()) {
return Result.failure(InvalidParameterException("notificationEventRequests cannot be empty"))
}
val json = runCatchingExceptions { json().encodeToString(notificationEventRequests.map { it.toData() }) }
.getOrElse {
Timber.e(it, "Failed to serialize notification requests")
return Result.failure(it)
}
val data = workerDataConverter.serialize(notificationEventRequests).getOrElse {
return Result.failure(it)
}
Timber.d("Scheduling ${notificationEventRequests.size} notification requests with WorkManager for $sessionId")
return Result.success(
OneTimeWorkRequestBuilder<FetchNotificationsWorker>()
.setInputData(workDataOf("requests" to json))
.setInputData(data)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
// TODO investigate using this instead of the resolver queue
@@ -64,23 +54,5 @@ class SyncNotificationWorkManagerRequest(
val eventId: String,
@SerialName("provider_info")
val providerInfo: String,
) {
fun toRequest(): NotificationEventRequest {
return NotificationEventRequest(
sessionId = SessionId(sessionId),
roomId = RoomId(roomId),
eventId = EventId(eventId),
providerInfo = providerInfo,
)
}
}
}
private fun NotificationEventRequest.toData(): SyncNotificationWorkManagerRequest.Data {
return SyncNotificationWorkManagerRequest.Data(
sessionId = sessionId.value,
roomId = roomId.value,
eventId = eventId.value,
providerInfo = providerInfo,
)
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.workmanager
import androidx.work.Data
import androidx.work.workDataOf
import dev.zacsweers.metro.Inject
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
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.push.api.push.NotificationEventRequest
import timber.log.Timber
@Inject
class WorkerDataConverter(
private val json: JsonProvider,
) {
fun serialize(notificationEventRequests: List<NotificationEventRequest>): Result<Data> {
return runCatchingExceptions { json().encodeToString(notificationEventRequests.map { it.toData() }) }
.onFailure {
Timber.e(it, "Failed to serialize notification requests")
}
.map { str ->
workDataOf(REQUESTS_KEY to str)
}
}
fun deserialize(data: Data): List<NotificationEventRequest>? {
val rawRequestsJson = data.getString(REQUESTS_KEY) ?: return null
return runCatchingExceptions {
json().decodeFromString<List<SyncNotificationWorkManagerRequest.Data>>(rawRequestsJson).map { it.toRequest() }
}.fold(
onSuccess = {
Timber.d("Deserialized ${it.size} requests")
it
},
onFailure = {
Timber.e(it, "Failed to deserialize notification requests")
null
}
)
}
companion object {
private const val REQUESTS_KEY = "requests"
}
}
private fun NotificationEventRequest.toData(): SyncNotificationWorkManagerRequest.Data {
return SyncNotificationWorkManagerRequest.Data(
sessionId = sessionId.value,
roomId = roomId.value,
eventId = eventId.value,
providerInfo = providerInfo,
)
}
private fun SyncNotificationWorkManagerRequest.Data.toRequest(): NotificationEventRequest {
return NotificationEventRequest(
sessionId = SessionId(sessionId),
roomId = RoomId(roomId),
eventId = EventId(eventId),
providerInfo = providerInfo,
)
}

View File

@@ -1,22 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M23.04,3.84C23.04,1.7192 24.7593,0 26.88,0C41.0185,0 52.48,11.4615 52.48,25.6C52.48,27.7208 50.7608,29.44 48.64,29.44C46.5193,29.44 44.8,27.7208 44.8,25.6C44.8,15.7031 36.777,7.68 26.88,7.68C24.7593,7.68 23.04,5.9608 23.04,3.84Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M40.96,60.16C40.96,62.2808 39.2407,64 37.12,64C22.9815,64 11.52,52.5385 11.52,38.4C11.52,36.2792 13.2392,34.56 15.36,34.56C17.4807,34.56 19.2,36.2792 19.2,38.4C19.2,48.2969 27.223,56.32 37.12,56.32C39.2407,56.32 40.96,58.0392 40.96,60.16Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M3.84,40.96C1.7192,40.96 -0,39.2407 -0,37.12C-0,22.9815 11.4615,11.52 25.6,11.52C27.7208,11.52 29.44,13.2392 29.44,15.36C29.44,17.4807 27.7208,19.2 25.6,19.2C15.7031,19.2 7.68,27.223 7.68,37.12C7.68,39.2407 5.9608,40.96 3.84,40.96Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M60.16,23.04C62.2808,23.04 64,24.7593 64,26.88C64,41.0185 52.5385,52.48 38.4,52.48C36.2792,52.48 34.56,50.7608 34.56,48.64C34.56,46.5193 36.2792,44.8 38.4,44.8C48.2969,44.8 56.32,36.777 56.32,26.88C56.32,24.7593 58.0392,23.04 60.16,23.04Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

View File

@@ -13,6 +13,7 @@ import androidx.core.app.NotificationCompat
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.test.A_COLOR_INT
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -52,6 +53,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
assertThat(result.number).isEqualTo(1)
@Suppress("DEPRECATION")
@@ -74,6 +76,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
@Suppress("DEPRECATION")
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT)
@@ -138,6 +141,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
assertThat(result.number).isEqualTo(1)
assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests)
@@ -156,6 +160,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
assertThat(result.number).isEqualTo(2)
assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10)
@@ -184,6 +189,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
val actionTitles = result.actions?.map { it.title }
assertThat(actionTitles).isEqualTo(
@@ -208,6 +214,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
)
assertThat(result.number).isEqualTo(1)
assertThat(result.`when`).isEqualTo(A_TIMESTAMP)

View File

@@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import androidx.core.app.NotificationManagerCompat
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -199,6 +200,7 @@ class DefaultNotificationDrawerManagerTest {
activeNotificationsProvider = activeNotificationsProvider,
stringProvider = FakeStringProvider(),
),
enterpriseService = FakeEnterpriseService(),
),
appNavigationStateService = appNavigationStateService,
coroutineScope = this,

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
@@ -55,6 +56,7 @@ class DefaultOnMissedCallNotificationHandlerTest {
notificationRenderer = NotificationRenderer(
notificationDisplayer = FakeNotificationDisplayer(),
notificationDataFactory = dataFactory,
enterpriseService = FakeEnterpriseService(),
),
appNavigationStateService = FakeAppNavigationStateService(),
coroutineScope = backgroundScope,

View File

@@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import androidx.core.app.NotificationCompat
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_COLOR_INT
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
@@ -47,6 +48,7 @@ class DefaultSummaryGroupMessageCreatorTest {
invitationNotifications = emptyList(),
simpleNotifications = emptyList(),
fallbackNotifications = emptyList(),
color = A_COLOR_INT,
)
notificationCreator.createSummaryListNotificationResult.assertions()

View File

@@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_COLOR_INT
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
@@ -53,7 +54,7 @@ class NotificationDataFactoryTest {
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT)
val roomInvitation = listOf(AN_INVITATION_EVENT)
val result = toNotifications(roomInvitation)
val result = toNotifications(roomInvitation, A_COLOR_INT)
assertThat(result).isEqualTo(
listOf(
@@ -73,7 +74,7 @@ class NotificationDataFactoryTest {
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT)
val roomInvitation = listOf(A_SIMPLE_EVENT)
val result = toNotifications(roomInvitation)
val result = toNotifications(roomInvitation, A_COLOR_INT)
assertThat(result).isEqualTo(
listOf(
@@ -93,11 +94,12 @@ class NotificationDataFactoryTest {
val events = listOf(A_MESSAGE_EVENT)
val expectedNotification = RoomNotification(
notification = fakeRoomGroupMessageCreator.createRoomMessage(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
events,
A_ROOM_ID,
FakeImageLoader().getImageLoader(),
null,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
events = events,
roomId = A_ROOM_ID,
imageLoader = FakeImageLoader().getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
),
roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!",
@@ -112,6 +114,7 @@ class NotificationDataFactoryTest {
messages = roomWithMessage,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
color = A_COLOR_INT,
)
assertThat(result.size).isEqualTo(1)
@@ -128,6 +131,7 @@ class NotificationDataFactoryTest {
messages = redactedRoom,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
color = A_COLOR_INT,
)
assertThat(result).isEmpty()
@@ -145,11 +149,12 @@ class NotificationDataFactoryTest {
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = RoomNotification(
notification = fakeRoomGroupMessageCreator.createRoomMessage(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
withRedactedRemoved,
A_ROOM_ID,
FakeImageLoader().getImageLoader(),
null,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
events = withRedactedRemoved,
roomId = A_ROOM_ID,
imageLoader = FakeImageLoader().getImageLoader(),
existingNotification = null,
color = A_COLOR_INT,
),
roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!",
@@ -163,6 +168,7 @@ class NotificationDataFactoryTest {
messages = roomWithRedactedMessage,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
color = A_COLOR_INT,
)
assertThat(result.size).isEqualTo(1)

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -58,6 +59,7 @@ class NotificationRendererTest {
private val notificationRenderer = NotificationRenderer(
notificationDisplayer = notificationDisplayer,
notificationDataFactory = notificationDataFactory,
enterpriseService = FakeEnterpriseService(),
)
@Test

View File

@@ -16,6 +16,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_COLOR_INT
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
@@ -50,7 +51,9 @@ class DefaultNotificationCreatorTest {
@Test
fun `test createDiagnosticNotification`() {
val sut = createNotificationCreator()
val result = sut.createDiagnosticNotification()
val result = sut.createDiagnosticNotification(
color = A_COLOR_INT,
)
result.commonAssertions(
expectedGroup = null,
expectedCategory = NotificationCompat.CATEGORY_STATUS,
@@ -72,7 +75,8 @@ class DefaultNotificationCreatorTest {
isUpdated = false,
timestamp = A_FAKE_TIMESTAMP,
cause = null,
)
),
color = A_COLOR_INT,
)
result.commonAssertions(
expectedCategory = null,
@@ -97,7 +101,8 @@ class DefaultNotificationCreatorTest {
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
)
),
color = A_COLOR_INT,
)
result.commonAssertions(
expectedCategory = null,
@@ -122,7 +127,8 @@ class DefaultNotificationCreatorTest {
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
)
),
color = A_COLOR_INT,
)
result.commonAssertions(
expectedCategory = null,
@@ -148,7 +154,8 @@ class DefaultNotificationCreatorTest {
isRedacted = false,
isUpdated = false,
roomName = "roomName",
)
),
color = A_COLOR_INT,
)
result.commonAssertions(
expectedCategory = null,
@@ -181,7 +188,8 @@ class DefaultNotificationCreatorTest {
isRedacted = false,
isUpdated = false,
roomName = "roomName",
)
),
color = A_COLOR_INT,
)
result.commonAssertions(
expectedCategory = null,
@@ -197,6 +205,7 @@ class DefaultNotificationCreatorTest {
compatSummary = "compatSummary",
noisy = false,
lastMessageTimestamp = 123_456L,
color = A_COLOR_INT,
)
result.commonAssertions(
expectedGroup = matrixUser.userId.value,
@@ -212,6 +221,7 @@ class DefaultNotificationCreatorTest {
compatSummary = "compatSummary",
noisy = true,
lastMessageTimestamp = 123_456L,
color = A_COLOR_INT,
)
result.commonAssertions(
expectedGroup = matrixUser.userId.value,
@@ -240,6 +250,7 @@ class DefaultNotificationCreatorTest {
existingNotification = null,
imageLoader = FakeImageLoader().getImageLoader(),
events = emptyList(),
color = A_COLOR_INT,
)
result.commonAssertions()
}
@@ -266,6 +277,7 @@ class DefaultNotificationCreatorTest {
existingNotification = null,
imageLoader = FakeImageLoader().getImageLoader(),
events = emptyList(),
color = A_COLOR_INT,
)
result.commonAssertions()
}

View File

@@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import android.graphics.Bitmap
import androidx.annotation.ColorInt
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -44,22 +45,32 @@ class FakeNotificationCreator(
currentUser: MatrixUser,
existingNotification: Notification?,
imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>
events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification {
return createMessagesListNotificationResult(
listOf(roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, currentUser, existingNotification, imageLoader, events)
)
}
override fun createRoomInvitationNotification(inviteNotifiableEvent: InviteNotifiableEvent): Notification {
override fun createRoomInvitationNotification(
inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification {
return createRoomInvitationNotificationResult(inviteNotifiableEvent)
}
override fun createSimpleEventNotification(simpleNotifiableEvent: SimpleNotifiableEvent): Notification {
override fun createSimpleEventNotification(
simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification {
return createSimpleNotificationResult(simpleNotifiableEvent)
}
override fun createFallbackNotification(fallbackNotifiableEvent: FallbackNotifiableEvent): Notification {
override fun createFallbackNotification(
fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification {
return createFallbackNotificationResult(fallbackNotifiableEvent)
}
@@ -67,12 +78,15 @@ class FakeNotificationCreator(
currentUser: MatrixUser,
compatSummary: String,
noisy: Boolean,
lastMessageTimestamp: Long
lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification {
return createSummaryListNotificationResult(currentUser, compatSummary, noisy, lastMessageTimestamp)
}
override fun createDiagnosticNotification(): Notification {
override fun createDiagnosticNotification(
@ColorInt color: Int,
): Notification {
return createDiagnosticNotificationResult()
}
}

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.push.impl.notifications.fake
import androidx.annotation.ColorInt
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.NotificationDataFactory
@@ -33,31 +34,45 @@ class FakeNotificationDataFactory(
List<OneShotNotification>,
List<OneShotNotification>,
SummaryNotification
> = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
> = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, List<OneShotNotification>> =
lambdaRecorder { _ -> emptyList() },
) : NotificationDataFactory {
override suspend fun toNotifications(messages: List<NotifiableMessageEvent>, currentUser: MatrixUser, imageLoader: ImageLoader): List<RoomNotification> {
override suspend fun toNotifications(
messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader,
@ColorInt color: Int,
): List<RoomNotification> {
return messageEventToNotificationsResult(messages, currentUser, imageLoader)
}
@JvmName("toNotificationInvites")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(invites: List<InviteNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
invites: List<InviteNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return inviteToNotificationsResult(invites)
}
@JvmName("toNotificationSimpleEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return simpleEventToNotificationsResult(simpleEvents)
}
@JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
override fun toNotifications(
fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int,
): List<OneShotNotification> {
return fallbackEventToNotificationsResult(fallback)
}
@@ -67,6 +82,7 @@ class FakeNotificationDataFactory(
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): SummaryNotification {
return summaryToNotificationsResult(
currentUser,

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import androidx.annotation.ColorInt
import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -19,14 +20,15 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeRoomGroupMessageCreator(
var createRoomMessageResult: LambdaFiveParamsRecorder<MatrixUser, List<NotifiableMessageEvent>, RoomId, ImageLoader, Notification?, Notification> =
lambdaRecorder { _, _, _, _, _, -> A_NOTIFICATION }
lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }
) : RoomGroupMessageCreator {
override suspend fun createRoomMessage(
currentUser: MatrixUser,
events: List<NotifiableMessageEvent>,
roomId: RoomId,
imageLoader: ImageLoader,
existingNotification: Notification?
existingNotification: Notification?,
@ColorInt color: Int,
): Notification {
return createRoomMessageResult(currentUser, events, roomId, imageLoader, existingNotification)
}

View File

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

View File

@@ -46,6 +46,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
@@ -715,7 +716,7 @@ class DefaultPushHandlerTest {
appCoroutineScope = backgroundScope,
workManagerScheduler = workManagerScheduler,
featureFlagService = featureFlagService,
json = DefaultJsonProvider(),
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
),
appCoroutineScope = backgroundScope,
fallbackNotificationFactory = FallbackNotificationFactory(

View File

@@ -8,6 +8,8 @@
package io.element.android.libraries.push.impl.troubleshoot
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@@ -64,10 +66,12 @@ class NotificationTestTest {
private fun createNotificationTest(): NotificationTest {
return NotificationTest(
sessionId = A_SESSION_ID,
notificationCreator = notificationCreator,
notificationDisplayer = fakeNotificationDisplayer,
notificationClickHandler = notificationClickHandler,
stringProvider = FakeStringProvider(),
enterpriseService = FakeEnterpriseService(),
)
}
}

View File

@@ -175,7 +175,7 @@ class FetchNotificationWorkerTest {
workManagerScheduler = workManagerScheduler,
syncOnNotifiableEvent = syncOnNotifiableEvent,
coroutineDispatchers = testCoroutineDispatchers(),
json = DefaultJsonProvider(),
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
)
private fun TestScope.createWorkerParams(

View File

@@ -49,14 +49,24 @@ class SyncNotificationWorkManagerRequestTest {
assertThat(result.isFailure).isTrue()
}
// TODO add test for invalid serialization (how?)
@Test
fun `build - invalid serialization`() = runTest {
val request = createSyncNotificationWorkManagerRequest(
sessionId = A_SESSION_ID,
notificationEventRequests = listOf(aNotificationEventRequest()),
workerDataConverter = WorkerDataConverter({ error("error during serialization") })
)
val result = request.build()
assertThat(result.isFailure).isTrue()
}
}
private fun createSyncNotificationWorkManagerRequest(
sessionId: SessionId,
notificationEventRequests: List<NotificationEventRequest>,
workerDataConverter: WorkerDataConverter = WorkerDataConverter(DefaultJsonProvider())
) = SyncNotificationWorkManagerRequest(
sessionId = sessionId,
notificationEventRequests = notificationEventRequests,
json = DefaultJsonProvider(),
workerDataConverter = workerDataConverter,
)

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.workmanager
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
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_SESSION_ID_2
import io.element.android.libraries.push.api.push.NotificationEventRequest
import org.junit.Test
class WorkerDataConverterTest {
@Test
fun `ensure identity when serializing - deserializing an empty list`() {
testIdentity(emptyList())
}
@Test
fun `ensure identity when serializing - deserializing a list`() {
testIdentity(
listOf(
NotificationEventRequest(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
providerInfo = "info1",
),
NotificationEventRequest(
sessionId = A_SESSION_ID_2,
roomId = A_ROOM_ID_2,
eventId = AN_EVENT_ID_2,
providerInfo = "info2",
),
)
)
}
private fun testIdentity(data: List<NotificationEventRequest>) {
val sut = WorkerDataConverter(DefaultJsonProvider())
val serialized = sut.serialize(data).getOrThrow()
val result = sut.deserialize(serialized)
assertThat(result).isEqualTo(data)
}
}