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()
.asFlow()
.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)
.asFlow()
.mapToOneNotNull()

View File

@ -1,5 +1,6 @@
package app.dapk.st.directory
import android.util.Log
import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember
@ -17,7 +18,11 @@ value class UnreadCount(val value: Int)
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(
private val syncService: SyncService,
@ -35,6 +40,7 @@ class DirectoryUseCase(
roomStore.observeUnreadCountById(),
syncService.events()
) { userId, overviewState, localEchos, unread, events ->
Log.e("!!!", "got states")
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
RoomFoo(
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 syncService() = syncService
fun credentialProvider() = credentialsStore
fun roomStore() = roomStore
fun iconLoader() = iconLoader
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
fun notificationsUseCase() = NotificationsUseCase(
roomStore,
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager,
iconLoader,
context,
NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context)),
NotificationChannels(notificationManager()),
)
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}

View File

@ -1,49 +1,26 @@
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.log
import app.dapk.st.imageloader.IconLoader
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.messenger.MessengerActivity
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
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(
private val roomStore: RoomStore,
private val notificationManager: NotificationManager,
private val iconLoader: IconLoader,
private val context: Context,
private val notificationRenderer: NotificationRenderer,
notificationChannels: NotificationChannels,
) {
private val inferredCurrentNotifications = mutableSetOf<RoomId>()
private val channelId = "message"
init {
if (notificationManager.getNotificationChannel(channelId) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
"messages",
NotificationManager.IMPORTANCE_HIGH,
)
)
}
notificationChannels.initChannels()
}
suspend fun listenForNotificationChanges() {
// TODO handle redactions by removing and edits by not notifying
roomStore.observeUnread()
.drop(1)
.onEach { result ->
@ -51,125 +28,12 @@ class NotificationsUseCase(
val asRooms = result.keys.map { it.roomId }.toSet()
val removedRooms = inferredCurrentNotifications - asRooms
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
inferredCurrentNotifications.clear()
inferredCurrentNotifications.addAll(asRooms)
val notifications = result.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
}
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)
}
notificationRenderer.render(result, removedRooms)
}
.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>
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
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>>
suspend fun observeEvent(eventId: EventId): Flow<EventId>
fun observeEvent(eventId: EventId): Flow<EventId>
suspend fun findEvent(eventId: EventId): RoomEvent?
}