splitting the notification logic

This commit is contained in:
Adam Brown 2022-03-13 18:56:21 +00:00
parent 6725a0648e
commit a8218b7e66
8 changed files with 199 additions and 150 deletions

View File

@ -75,7 +75,7 @@ internal class RoomPersistence(
} }
} }
override suspend fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> { override fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
return database.roomEventQueries.selectAllUnread() return database.roomEventQueries.selectAllUnread()
.asFlow() .asFlow()
.mapToList() .mapToList()
@ -107,7 +107,7 @@ internal class RoomPersistence(
} }
} }
override suspend fun observeEvent(eventId: EventId): Flow<EventId> { override fun observeEvent(eventId: EventId): Flow<EventId> {
return database.roomEventQueries.selectEvent(event_id = eventId.value) return database.roomEventQueries.selectEvent(event_id = eventId.value)
.asFlow() .asFlow()
.mapToOneNotNull() .mapToOneNotNull()

View File

@ -1,5 +1,6 @@
package app.dapk.st.directory package app.dapk.st.directory
import android.util.Log
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
@ -17,7 +18,11 @@ value class UnreadCount(val value: Int)
typealias DirectoryState = List<RoomFoo> typealias DirectoryState = List<RoomFoo>
data class RoomFoo(val overview: RoomOverview, val unreadCount: UnreadCount, val typing: Typing?) data class RoomFoo(
val overview: RoomOverview,
val unreadCount: UnreadCount,
val typing: Typing?
)
class DirectoryUseCase( class DirectoryUseCase(
private val syncService: SyncService, private val syncService: SyncService,
@ -35,6 +40,7 @@ class DirectoryUseCase(
roomStore.observeUnreadCountById(), roomStore.observeUnreadCountById(),
syncService.events() syncService.events()
) { userId, overviewState, localEchos, unread, events -> ) { userId, overviewState, localEchos, unread, events ->
Log.e("!!!", "got states")
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview -> overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
RoomFoo( RoomFoo(
overview = roomOverview, overview = roomOverview,

View File

@ -0,0 +1,24 @@
package app.dapk.st.notifications
import android.app.NotificationChannel
import android.app.NotificationManager
private const val channelId = "message"
class NotificationChannels(
private val notificationManager: NotificationManager
) {
fun initChannels() {
if (notificationManager.getNotificationChannel(channelId) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
"messages",
NotificationManager.IMPORTANCE_HIGH,
)
)
}
}
}

View File

@ -0,0 +1,113 @@
package app.dapk.st.notifications
import android.app.Notification
import android.app.PendingIntent
import android.app.Person
import android.content.Context
import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.messenger.MessengerActivity
private const val GROUP_ID = "st"
private const val channelId = "message"
class NotificationFactory(
private val iconLoader: IconLoader,
private val context: Context,
) {
suspend fun createNotifications(events: Map<RoomOverview, List<RoomEvent>>): Notifications {
val notifications = events.map { (roomOverview, events) ->
val messageEvents = events.filterIsInstance<RoomEvent.Message>()
when (messageEvents.isEmpty()) {
true -> NotificationDelegate.DismissRoom(roomOverview.roomId)
false -> createNotification(messageEvents, roomOverview)
}
}
val summaryNotification = if (notifications.filterIsInstance<NotificationDelegate.Room>().size > 1) {
createSummary(notifications)
} else {
null
}
return Notifications(summaryNotification, notifications)
}
private fun createSummary(notifications: List<NotificationDelegate>): Notification {
val summaryInboxStyle = Notification.InboxStyle().also { style ->
notifications.forEach {
when (it) {
is NotificationDelegate.DismissRoom -> {
// do nothing
}
is NotificationDelegate.Room -> style.addLine(it.summary)
}
}
}
return Notification.Builder(context, channelId)
.setStyle(summaryInboxStyle)
.setSmallIcon(R.drawable.ic_notification_small_icon)
.setCategory(Notification.CATEGORY_MESSAGE)
.setGroupSummary(true)
.setGroup(GROUP_ID)
.build()
}
private suspend fun createNotification(events: List<RoomEvent.Message>, roomOverview: RoomOverview): NotificationDelegate {
val messageStyle = Notification.MessagingStyle(
Person.Builder()
.setName("me")
.setKey(roomOverview.roomId.value)
.build()
)
messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup }
messageStyle.isGroupConversation = roomOverview.isGroup
events.sortedBy { it.utcTimestamp }.forEach { message ->
val sender = Person.Builder()
.setName(message.author.displayName ?: message.author.id.value)
.setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) })
.setKey(message.author.id.value)
.build()
messageStyle.addMessage(
Notification.MessagingStyle.Message(
message.content,
message.utcTimestamp,
sender,
)
)
}
val openRoomIntent = PendingIntent.getActivity(
context,
55,
MessengerActivity.newInstance(context, roomOverview.roomId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationDelegate.Room(
Notification.Builder(context, channelId)
.setWhen(messageStyle.messages.last().timestamp)
.setShowWhen(true)
.setGroup(GROUP_ID)
.setOnlyAlertOnce(roomOverview.isGroup)
.setContentIntent(openRoomIntent)
.setStyle(messageStyle)
.setCategory(Notification.CATEGORY_MESSAGE)
.setShortcutId(roomOverview.roomId.value)
.setSmallIcon(R.drawable.ic_notification_small_icon)
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
.setAutoCancel(true)
.build(),
roomId = roomOverview.roomId,
summary = messageStyle.messages.last().text.toString()
)
}
}
data class Notifications(val summaryNotification: Notification?, val delegates: List<NotificationDelegate>)

View File

@ -0,0 +1,43 @@
package app.dapk.st.notifications
import android.app.Notification
import android.app.NotificationManager
import app.dapk.st.core.extensions.ifNull
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
private const val SUMMARY_NOTIFICATION_ID = 101
private const val MESSAGE_NOTIFICATION_ID = 100
class NotificationRenderer(
private val notificationManager: NotificationManager,
private val notificationFactory: NotificationFactory,
) {
suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>) {
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
val notifications = notificationFactory.createNotifications(result)
notifications.summaryNotification.ifNull {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
}
notifications.delegates.forEach {
when (it) {
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
}
}
notifications.summaryNotification?.let {
notificationManager.notify(SUMMARY_NOTIFICATION_ID, it)
}
}
}
sealed interface NotificationDelegate {
data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate
data class DismissRoom(val roomId: RoomId) : NotificationDelegate
}

View File

@ -23,13 +23,12 @@ class NotificationsModule(
fun pushUseCase() = pushService fun pushUseCase() = pushService
fun syncService() = syncService fun syncService() = syncService
fun credentialProvider() = credentialsStore fun credentialProvider() = credentialsStore
fun roomStore() = roomStore
fun iconLoader() = iconLoader
fun firebasePushTokenUseCase() = firebasePushTokenUseCase fun firebasePushTokenUseCase() = firebasePushTokenUseCase
fun notificationsUseCase() = NotificationsUseCase( fun notificationsUseCase() = NotificationsUseCase(
roomStore, roomStore,
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context)),
iconLoader, NotificationChannels(notificationManager()),
context,
) )
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
} }

View File

@ -1,49 +1,26 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import android.app.*
import android.app.Notification.InboxStyle
import android.content.Context
import app.dapk.st.core.AppLogTag.NOTIFICATION import app.dapk.st.core.AppLogTag.NOTIFICATION
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.messenger.MessengerActivity
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
private const val SUMMARY_NOTIFICATION_ID = 101
private const val MESSAGE_NOTIFICATION_ID = 100
private const val GROUP_ID = "st"
class NotificationsUseCase( class NotificationsUseCase(
private val roomStore: RoomStore, private val roomStore: RoomStore,
private val notificationManager: NotificationManager, private val notificationRenderer: NotificationRenderer,
private val iconLoader: IconLoader, notificationChannels: NotificationChannels,
private val context: Context,
) { ) {
private val inferredCurrentNotifications = mutableSetOf<RoomId>() private val inferredCurrentNotifications = mutableSetOf<RoomId>()
private val channelId = "message"
init { init {
if (notificationManager.getNotificationChannel(channelId) == null) { notificationChannels.initChannels()
notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
"messages",
NotificationManager.IMPORTANCE_HIGH,
)
)
}
} }
suspend fun listenForNotificationChanges() { suspend fun listenForNotificationChanges() {
// TODO handle redactions by removing and edits by not notifying
roomStore.observeUnread() roomStore.observeUnread()
.drop(1) .drop(1)
.onEach { result -> .onEach { result ->
@ -51,125 +28,12 @@ class NotificationsUseCase(
val asRooms = result.keys.map { it.roomId }.toSet() val asRooms = result.keys.map { it.roomId }.toSet()
val removedRooms = inferredCurrentNotifications - asRooms val removedRooms = inferredCurrentNotifications - asRooms
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
inferredCurrentNotifications.clear() inferredCurrentNotifications.clear()
inferredCurrentNotifications.addAll(asRooms) inferredCurrentNotifications.addAll(asRooms)
val notifications = result.map { (roomOverview, events) -> notificationRenderer.render(result, removedRooms)
val messageEvents = events.filterIsInstance<RoomEvent.Message>()
when (messageEvents.isEmpty()) {
true -> NotificationDelegate.DismissRoom(roomOverview.roomId)
false -> createNotification(messageEvents, roomOverview)
}
}
val summaryNotification = if (notifications.filterIsInstance<NotificationDelegate.Room>().size > 1) {
createSummary(notifications)
} else {
null
}
if (summaryNotification == null) {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
}
notifications.forEach {
when (it) {
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
}
}
if (summaryNotification != null) {
notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification)
}
} }
.collect() .collect()
} }
private fun createSummary(notifications: List<NotificationDelegate>): Notification {
val summaryInboxStyle = InboxStyle().also { style ->
notifications.forEach {
when (it) {
is NotificationDelegate.DismissRoom -> {
// do nothing
}
is NotificationDelegate.Room -> style.addLine(it.summary)
}
}
}
return Notification.Builder(context, channelId)
.setStyle(summaryInboxStyle)
.setSmallIcon(R.drawable.ic_notification_small_icon)
.setCategory(Notification.CATEGORY_MESSAGE)
.setGroupSummary(true)
.setGroup(GROUP_ID)
.build()
}
private suspend fun createNotification(events: List<RoomEvent.Message>, roomOverview: RoomOverview): NotificationDelegate {
val messageStyle = Notification.MessagingStyle(
Person.Builder()
.setName("me")
.setKey(roomOverview.roomId.value)
.build()
)
messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup }
messageStyle.isGroupConversation = roomOverview.isGroup
events.sortedBy { it.utcTimestamp }.forEach { message ->
val sender = Person.Builder()
.setName(message.author.displayName ?: message.author.id.value)
.setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) })
.setKey(message.author.id.value)
.build()
messageStyle.addMessage(
Notification.MessagingStyle.Message(
message.content,
message.utcTimestamp,
sender,
)
)
}
val openRoomIntent = PendingIntent.getActivity(
context,
55,
MessengerActivity.newInstance(context, roomOverview.roomId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationDelegate.Room(
Notification.Builder(context, channelId)
.setWhen(messageStyle.messages.last().timestamp)
.setShowWhen(true)
.setGroup(GROUP_ID)
.setOnlyAlertOnce(roomOverview.isGroup)
.setContentIntent(openRoomIntent)
.setStyle(messageStyle)
.setCategory(Notification.CATEGORY_MESSAGE)
.setShortcutId(roomOverview.roomId.value)
.setSmallIcon(R.drawable.ic_notification_small_icon)
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
.setAutoCancel(true)
.build(),
roomId = roomOverview.roomId,
summary = messageStyle.messages.last().text.toString()
)
}
}
sealed interface NotificationDelegate {
data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate
data class DismissRoom(val roomId: RoomId) : NotificationDelegate
} }

View File

@ -12,9 +12,9 @@ interface RoomStore {
fun latest(roomId: RoomId): Flow<RoomState> fun latest(roomId: RoomId): Flow<RoomState>
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>) suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
suspend fun markRead(roomId: RoomId) suspend fun markRead(roomId: RoomId)
suspend fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>>
fun observeUnreadCountById(): Flow<Map<RoomId, Int>> fun observeUnreadCountById(): Flow<Map<RoomId, Int>>
suspend fun observeEvent(eventId: EventId): Flow<EventId> fun observeEvent(eventId: EventId): Flow<EventId>
suspend fun findEvent(eventId: EventId): RoomEvent? suspend fun findEvent(eventId: EventId): RoomEvent?
} }