improved notification testing
This commit is contained in:
parent
bb47fcd9d6
commit
65bf8c0d64
|
@ -1,15 +1,14 @@
|
||||||
package app.dapk.st.graph
|
package app.dapk.st.graph
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import app.dapk.db.DapkDb
|
import app.dapk.db.DapkDb
|
||||||
import app.dapk.st.BuildConfig
|
import app.dapk.st.BuildConfig
|
||||||
import app.dapk.st.SharedPreferencesDelegate
|
import app.dapk.st.SharedPreferencesDelegate
|
||||||
import app.dapk.st.core.BuildMeta
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.core.CoreAndroidModule
|
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
|
||||||
import app.dapk.st.core.SingletonFlows
|
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
|
@ -89,6 +88,22 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
|
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
|
||||||
|
|
||||||
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
||||||
|
override fun notificationOpenApp(context: Context) = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
1000,
|
||||||
|
home(context)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun notificationOpenMessage(context: Context, roomId: RoomId) = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
roomId.hashCode(),
|
||||||
|
messenger(context, roomId)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
override fun home(context: Context) = Intent(context, MainActivity::class.java)
|
override fun home(context: Context) = Intent(context, MainActivity::class.java)
|
||||||
override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId)
|
override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId)
|
||||||
override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId)
|
override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId)
|
||||||
|
@ -175,6 +190,7 @@ internal class FeatureModules internal constructor(
|
||||||
workModule.workScheduler(),
|
workModule.workScheduler(),
|
||||||
intentFactory = coreAndroidModule.intentFactory(),
|
intentFactory = coreAndroidModule.intentFactory(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
|
deviceMeta = DeviceMeta(Build.VERSION.SDK_INT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,4 +3,4 @@ package app.dapk.st.core
|
||||||
data class BuildMeta(
|
data class BuildMeta(
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
data class DeviceMeta(
|
||||||
|
val apiVersion: Int
|
||||||
|
)
|
|
@ -1,9 +1,6 @@
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import io.mockk.MockKMatcherScope
|
import io.mockk.*
|
||||||
import io.mockk.MockKVerificationScope
|
|
||||||
import io.mockk.coJustRun
|
|
||||||
import io.mockk.coVerify
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
@ -15,9 +12,11 @@ fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
|
||||||
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
|
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
|
||||||
|
|
||||||
private val expects = mutableListOf<Pair<Int, suspend MockKVerificationScope.() -> Unit>>()
|
private val expects = mutableListOf<Pair<Int, suspend MockKVerificationScope.() -> Unit>>()
|
||||||
|
private val groups = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
|
||||||
|
|
||||||
override fun verifyExpects() = expects.forEach { (times, block) ->
|
override fun verifyExpects() {
|
||||||
coVerify(exactly = times) { block.invoke(this) }
|
expects.forEach { (times, block) -> coVerify(exactly = times) { block.invoke(this) } }
|
||||||
|
groups.forEach { coVerifyOrder { it.invoke(this) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> T.expectUnit(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
|
override fun <T> T.expectUnit(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||||
|
@ -25,6 +24,9 @@ class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestSc
|
||||||
expects.add(times to { block(this@expectUnit) })
|
expects.add(times to { block(this@expectUnit) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||||
|
groups.add { block(this@captureExpects) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Any.ignore() = Unit
|
private fun Any.ignore() = Unit
|
||||||
|
@ -32,4 +34,5 @@ private fun Any.ignore() = Unit
|
||||||
interface ExpectTestScope : CoroutineScope {
|
interface ExpectTestScope : CoroutineScope {
|
||||||
fun verifyExpects()
|
fun verifyExpects()
|
||||||
fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
|
fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
|
||||||
|
fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit)
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
fun <T> DeviceMeta.isAtLeastO(block: () -> T, fallback: () -> T = { throw IllegalStateException("not handled") }): T {
|
||||||
|
return if (this.apiVersion >= Build.VERSION_CODES.O) block() else fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DeviceMeta.onAtLeastO(block: () -> Unit) {
|
||||||
|
if (this.apiVersion >= Build.VERSION_CODES.O) block()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> DeviceMeta.whenPOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.P, block, fallback)
|
||||||
|
inline fun <T> DeviceMeta.whenOOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.O, block, fallback)
|
||||||
|
|
||||||
|
inline fun <T> DeviceMeta.whenXOrHigher(version: Int, block: () -> T, fallback: () -> T): T {
|
||||||
|
return if (this.apiVersion >= version) block() else fallback()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeContext {
|
||||||
|
val instance = mockk<Context>()
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
|
||||||
|
class FakeInboxStyle {
|
||||||
|
private val _summary = slot<String>()
|
||||||
|
|
||||||
|
val instance = mockk<Notification.InboxStyle>()
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
val summary: String
|
||||||
|
get() = _summary.captured
|
||||||
|
|
||||||
|
fun captureInteractions() {
|
||||||
|
every { instance.addLine(capture(lines)) } returns instance
|
||||||
|
every { instance.setSummaryText(capture(_summary)) } returns instance
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.Person
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeMessagingStyle {
|
||||||
|
var user: Person? = null
|
||||||
|
val instance = mockk<Notification.MessagingStyle>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aFakeMessagingStyle() = FakeMessagingStyle().instance
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeNotificationBuilder {
|
||||||
|
val instance = mockk<Notification.Builder>(relaxed = true)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Person
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakePersonBuilder {
|
||||||
|
val instance = mockk<Person.Builder>(relaxed = true)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package app.dapk.st.navigator
|
package app.dapk.st.navigator
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
@ -40,6 +41,8 @@ interface Navigator {
|
||||||
|
|
||||||
interface IntentFactory {
|
interface IntentFactory {
|
||||||
|
|
||||||
|
fun notificationOpenApp(context: Context): PendingIntent
|
||||||
|
fun notificationOpenMessage(context: Context, roomId: RoomId): PendingIntent
|
||||||
fun home(context: Context): Intent
|
fun home(context: Context): Intent
|
||||||
fun messenger(context: Context, roomId: RoomId): Intent
|
fun messenger(context: Context, roomId: RoomId): Intent
|
||||||
fun messengerShortcut(context: Context, roomId: RoomId): Intent
|
fun messengerShortcut(context: Context, roomId: RoomId): Intent
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.core.isAtLeastO
|
||||||
|
import app.dapk.st.core.onAtLeastO
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
@Suppress("ObjectPropertyName")
|
||||||
|
private val _builderFactory: (Context, String, DeviceMeta) -> Notification.Builder = { context, channel, deviceMeta ->
|
||||||
|
deviceMeta.isAtLeastO(
|
||||||
|
block = { Notification.Builder(context, channel) },
|
||||||
|
fallback = { Notification.Builder(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidNotificationBuilder(
|
||||||
|
private val context: Context,
|
||||||
|
private val deviceMeta: DeviceMeta,
|
||||||
|
private val notificationStyleBuilder: AndroidNotificationStyleBuilder,
|
||||||
|
private val builderFactory: (Context, String, DeviceMeta) -> Notification.Builder = _builderFactory
|
||||||
|
) {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun build(notification: AndroidNotification): Notification {
|
||||||
|
return builder(notification.channelId)
|
||||||
|
.apply { setOnlyAlertOnce(!notification.alertMoreThanOnce) }
|
||||||
|
.apply { setAutoCancel(notification.autoCancel) }
|
||||||
|
.apply { setGroupSummary(notification.isGroupSummary) }
|
||||||
|
.ifNotNull(notification.groupId) { setGroup(it) }
|
||||||
|
.ifNotNull(notification.messageStyle) { style = it.build(notificationStyleBuilder) }
|
||||||
|
.ifNotNull(notification.contentIntent) { setContentIntent(it) }
|
||||||
|
.ifNotNull(notification.whenTimestamp) {
|
||||||
|
setShowWhen(true)
|
||||||
|
setWhen(it)
|
||||||
|
}
|
||||||
|
.ifNotNull(notification.category) { setCategory(it) }
|
||||||
|
.ifNotNull(notification.shortcutId) {
|
||||||
|
deviceMeta.onAtLeastO { setShortcutId(notification.shortcutId) }
|
||||||
|
}
|
||||||
|
.ifNotNull(notification.smallIcon) { setSmallIcon(it) }
|
||||||
|
.ifNotNull(notification.largeIcon) { setLargeIcon(it) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Notification.Builder.ifNotNull(value: T?, action: Notification.Builder.(T) -> Unit): Notification.Builder {
|
||||||
|
if (value != null) {
|
||||||
|
action(value)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun builder(channel: String) = builderFactory(context, channel, deviceMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AndroidNotification(
|
||||||
|
val channelId: String,
|
||||||
|
val whenTimestamp: Long? = null,
|
||||||
|
val isGroupSummary: Boolean = false,
|
||||||
|
val groupId: String? = null,
|
||||||
|
val groupAlertBehavior: Int? = null,
|
||||||
|
val shortcutId: String? = null,
|
||||||
|
val alertMoreThanOnce: Boolean,
|
||||||
|
val contentIntent: PendingIntent? = null,
|
||||||
|
val messageStyle: AndroidNotificationStyle? = null,
|
||||||
|
val category: String? = null,
|
||||||
|
val smallIcon: Int? = null,
|
||||||
|
val largeIcon: Icon? = null,
|
||||||
|
val autoCancel: Boolean = true,
|
||||||
|
) {
|
||||||
|
fun build(builder: AndroidNotificationBuilder) = builder.build(this)
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
sealed interface AndroidNotificationStyle {
|
||||||
|
|
||||||
|
fun build(builder: AndroidNotificationStyleBuilder): Notification.Style
|
||||||
|
|
||||||
|
data class Inbox(val lines: List<String>, val summary: String? = null) : AndroidNotificationStyle {
|
||||||
|
override fun build(builder: AndroidNotificationStyleBuilder) = builder.build(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Messaging(
|
||||||
|
val person: AndroidPerson,
|
||||||
|
val title: String?,
|
||||||
|
val isGroup: Boolean,
|
||||||
|
val content: List<AndroidMessage>,
|
||||||
|
) : AndroidNotificationStyle {
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
override fun build(builder: AndroidNotificationStyleBuilder) = builder.build(this)
|
||||||
|
|
||||||
|
data class AndroidPerson(val name: String, val key: String, val icon: Icon? = null)
|
||||||
|
data class AndroidMessage(val sender: AndroidPerson, val content: String, val timestamp: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.Notification.InboxStyle
|
||||||
|
import android.app.Notification.MessagingStyle
|
||||||
|
import android.app.Person
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
class AndroidNotificationStyleBuilder(
|
||||||
|
private val personBuilderFactory: () -> Person.Builder = { Person.Builder() },
|
||||||
|
private val inboxStyleFactory: () -> InboxStyle = { InboxStyle() },
|
||||||
|
private val messagingStyleFactory: (Person) -> MessagingStyle = { MessagingStyle(it) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun build(style: AndroidNotificationStyle): Notification.Style {
|
||||||
|
return when (style) {
|
||||||
|
is AndroidNotificationStyle.Inbox -> style.buildInboxStyle()
|
||||||
|
is AndroidNotificationStyle.Messaging -> style.buildMessagingStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AndroidNotificationStyle.Inbox.buildInboxStyle() = inboxStyleFactory().also { inboxStyle ->
|
||||||
|
lines.forEach { inboxStyle.addLine(it) }
|
||||||
|
inboxStyle.setSummaryText(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
private fun AndroidNotificationStyle.Messaging.buildMessagingStyle() = messagingStyleFactory(
|
||||||
|
personBuilderFactory()
|
||||||
|
.setName(person.name)
|
||||||
|
.setKey(person.key)
|
||||||
|
.build()
|
||||||
|
).also { style ->
|
||||||
|
content.forEach {
|
||||||
|
val sender = personBuilderFactory()
|
||||||
|
.setName(it.sender.name)
|
||||||
|
.setKey(it.sender.key)
|
||||||
|
.setIcon(it.sender.icon)
|
||||||
|
.build()
|
||||||
|
style.addMessage(MessagingStyle.Message(it.content, it.timestamp, sender))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,198 +1,58 @@
|
||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Person
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import app.dapk.st.core.DeviceMeta
|
||||||
import android.os.Build
|
import app.dapk.st.core.whenPOrHigher
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import app.dapk.st.imageloader.IconLoader
|
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.common.RoomMember
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
|
|
||||||
private const val GROUP_ID = "st"
|
private const val GROUP_ID = "st"
|
||||||
private const val channelId = "message"
|
private const val channelId = "message"
|
||||||
|
|
||||||
class NotificationFactory(
|
class NotificationFactory(
|
||||||
private val iconLoader: IconLoader,
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val notificationStyleFactory: NotificationStyleFactory,
|
||||||
private val intentFactory: IntentFactory,
|
private val intentFactory: IntentFactory,
|
||||||
|
private val iconLoader: IconLoader,
|
||||||
|
private val deviceMeta: DeviceMeta,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val shouldAlwaysAlertDms = true
|
private val shouldAlwaysAlertDms = true
|
||||||
|
|
||||||
private fun RoomEvent.toNotifiableContent(): String = when (this) {
|
suspend fun createMessageNotification(
|
||||||
is RoomEvent.Image -> "\uD83D\uDCF7"
|
|
||||||
is RoomEvent.Message -> this.content
|
|
||||||
is RoomEvent.Reply -> this.message.toNotifiableContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createNotifications(allUnread: Map<RoomOverview, List<RoomEvent>>, roomsWithNewEvents: Set<RoomId>, newRooms: Set<RoomId>): Notifications {
|
|
||||||
val notifications = allUnread.map { (roomOverview, events) ->
|
|
||||||
val messageEvents = events.map {
|
|
||||||
when (it) {
|
|
||||||
is RoomEvent.Image -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
|
||||||
is RoomEvent.Message -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
|
||||||
is RoomEvent.Reply -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when (messageEvents.isEmpty()) {
|
|
||||||
true -> NotificationDelegate.DismissRoom(roomOverview.roomId)
|
|
||||||
false -> createMessageNotification(messageEvents, roomOverview, roomsWithNewEvents, newRooms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val summaryNotification = if (notifications.filterIsInstance<NotificationDelegate.Room>().isNotEmpty()) {
|
|
||||||
val isAlerting = notifications.any { it is NotificationDelegate.Room && it.isAlerting }
|
|
||||||
createSummary(notifications, isAlerting = isAlerting)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
return Notifications(summaryNotification, notifications)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSummary(notifications: List<NotificationDelegate>, isAlerting: Boolean): Notification {
|
|
||||||
val summaryInboxStyle = Notification.InboxStyle().also { style ->
|
|
||||||
notifications.sortedBy {
|
|
||||||
when (it) {
|
|
||||||
is NotificationDelegate.DismissRoom -> -1
|
|
||||||
is NotificationDelegate.Room -> it.notification.`when`
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
when (it) {
|
|
||||||
is NotificationDelegate.DismissRoom -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
is NotificationDelegate.Room -> {
|
|
||||||
style.addLine(it.summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (notifications.size > 1) {
|
|
||||||
summaryInboxStyle.setSummaryText("${notifications.countMessages()} messages from ${notifications.size} chats")
|
|
||||||
}
|
|
||||||
|
|
||||||
val openAppIntent = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
1000,
|
|
||||||
intentFactory.home(context)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
return builder()
|
|
||||||
.setStyle(summaryInboxStyle)
|
|
||||||
.setOnlyAlertOnce(!isAlerting)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
|
||||||
.setGroupSummary(true)
|
|
||||||
.setGroup(GROUP_ID)
|
|
||||||
.setContentIntent(openAppIntent)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<NotificationDelegate>.countMessages() = this.sumOf {
|
|
||||||
when (it) {
|
|
||||||
is NotificationDelegate.DismissRoom -> 0
|
|
||||||
is NotificationDelegate.Room -> it.messageCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
|
||||||
private suspend fun createMessageStyle(events: List<Notifiable>, roomOverview: RoomOverview): Notification.MessagingStyle {
|
|
||||||
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.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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return messageStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createMessageNotification(
|
|
||||||
events: List<Notifiable>,
|
events: List<Notifiable>,
|
||||||
roomOverview: RoomOverview,
|
roomOverview: RoomOverview,
|
||||||
roomsWithNewEvents: Set<RoomId>,
|
roomsWithNewEvents: Set<RoomId>,
|
||||||
newRooms: Set<RoomId>
|
newRooms: Set<RoomId>
|
||||||
): NotificationDelegate {
|
): NotificationTypes {
|
||||||
val sortedEvents = events.sortedBy { it.utcTimestamp }
|
val sortedEvents = events.sortedBy { it.utcTimestamp }
|
||||||
|
val messageStyle = notificationStyleFactory.message(sortedEvents, roomOverview)
|
||||||
val messageStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
val openRoomIntent = intentFactory.notificationOpenMessage(context, roomOverview.roomId)
|
||||||
createMessageStyle(sortedEvents, roomOverview)
|
|
||||||
} else {
|
|
||||||
val inboxStyle = Notification.InboxStyle()
|
|
||||||
sortedEvents.forEach {
|
|
||||||
inboxStyle.addLine("${it.author.displayName ?: it.author.id.value}: ${it.content}")
|
|
||||||
}
|
|
||||||
inboxStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
val openRoomIntent = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
roomOverview.roomId.hashCode(),
|
|
||||||
MessengerActivity.newInstance(context, roomOverview.roomId)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
val shouldAlertMoreThanOnce = when {
|
val shouldAlertMoreThanOnce = when {
|
||||||
roomOverview.isDm() -> roomsWithNewEvents.contains(roomOverview.roomId) && shouldAlwaysAlertDms
|
roomOverview.isDm() -> roomsWithNewEvents.contains(roomOverview.roomId) && shouldAlwaysAlertDms
|
||||||
else -> newRooms.contains(roomOverview.roomId)
|
else -> newRooms.contains(roomOverview.roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NotificationDelegate.Room(
|
return NotificationTypes.Room(
|
||||||
builder()
|
AndroidNotification(
|
||||||
.setWhen(sortedEvents.last().utcTimestamp)
|
channelId = channelId,
|
||||||
.setShowWhen(true)
|
whenTimestamp = sortedEvents.last().utcTimestamp,
|
||||||
.setGroup(GROUP_ID)
|
groupId = GROUP_ID,
|
||||||
.run {
|
groupAlertBehavior = deviceMeta.whenPOrHigher(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
block = { Notification.GROUP_ALERT_SUMMARY },
|
||||||
this.setGroupAlertBehavior(Notification.GROUP_ALERT_SUMMARY)
|
fallback = { null }
|
||||||
} else {
|
),
|
||||||
this
|
shortcutId = roomOverview.roomId.value,
|
||||||
}
|
alertMoreThanOnce = shouldAlertMoreThanOnce,
|
||||||
}
|
contentIntent = openRoomIntent,
|
||||||
.setOnlyAlertOnce(!shouldAlertMoreThanOnce)
|
messageStyle = messageStyle,
|
||||||
.setContentIntent(openRoomIntent)
|
category = Notification.CATEGORY_MESSAGE,
|
||||||
.setStyle(messageStyle)
|
smallIcon = R.drawable.ic_notification_small_icon,
|
||||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
largeIcon = roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) },
|
||||||
.run {
|
autoCancel = true
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
),
|
||||||
this.setShortcutId(roomOverview.roomId.value)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
|
||||||
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.build(),
|
|
||||||
roomId = roomOverview.roomId,
|
roomId = roomOverview.roomId,
|
||||||
summary = sortedEvents.last().content,
|
summary = sortedEvents.last().content,
|
||||||
messageCount = sortedEvents.size,
|
messageCount = sortedEvents.size,
|
||||||
|
@ -200,16 +60,19 @@ class NotificationFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun builder(channel: String = channelId) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
fun createSummary(notifications: List<NotificationTypes.Room>): AndroidNotification {
|
||||||
Notification.Builder(context, channel)
|
val summaryInboxStyle = notificationStyleFactory.summary(notifications)
|
||||||
} else {
|
val openAppIntent = intentFactory.notificationOpenApp(context)
|
||||||
Notification.Builder(context)
|
return AndroidNotification(
|
||||||
|
channelId = channelId,
|
||||||
|
messageStyle = summaryInboxStyle,
|
||||||
|
alertMoreThanOnce = notifications.any { it.isAlerting },
|
||||||
|
smallIcon = R.drawable.ic_notification_small_icon,
|
||||||
|
contentIntent = openAppIntent,
|
||||||
|
groupId = GROUP_ID,
|
||||||
|
isGroupSummary = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomOverview.isDm() = !this.isGroup
|
private fun RoomOverview.isDm() = !this.isGroup
|
||||||
|
|
||||||
data class Notifications(val summaryNotification: Notification?, val delegates: List<NotificationDelegate>)
|
|
||||||
|
|
||||||
data class Notifiable(val content: String, val utcTimestamp: Long, val author: RoomMember)
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import app.dapk.st.core.AppLogTag
|
import app.dapk.st.core.AppLogTag
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
@ -9,7 +8,6 @@ import app.dapk.st.core.log
|
||||||
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.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
private const val SUMMARY_NOTIFICATION_ID = 101
|
private const val SUMMARY_NOTIFICATION_ID = 101
|
||||||
|
@ -17,13 +15,14 @@ private const val MESSAGE_NOTIFICATION_ID = 100
|
||||||
|
|
||||||
class NotificationRenderer(
|
class NotificationRenderer(
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
private val notificationFactory: NotificationFactory,
|
private val notificationStateMapper: NotificationStateMapper,
|
||||||
|
private val androidNotificationBuilder: AndroidNotificationBuilder,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun render(allUnread: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>, roomsWithNewEvents: Set<RoomId>, newRooms: Set<RoomId>) {
|
suspend fun render(state: NotificationState) {
|
||||||
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
state.removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
||||||
val notifications = notificationFactory.createNotifications(allUnread, roomsWithNewEvents, newRooms)
|
val notifications = notificationStateMapper.mapToNotifications(state)
|
||||||
|
|
||||||
withContext(dispatchers.main) {
|
withContext(dispatchers.main) {
|
||||||
notifications.summaryNotification.ifNull {
|
notifications.summaryNotification.ifNull {
|
||||||
|
@ -31,31 +30,46 @@ class NotificationRenderer(
|
||||||
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onlyContainsRemovals = removedRooms.isNotEmpty() && roomsWithNewEvents.isEmpty()
|
val onlyContainsRemovals = state.onlyContainsRemovals()
|
||||||
notifications.delegates.forEach {
|
notifications.delegates.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
is NotificationTypes.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
||||||
is NotificationDelegate.Room -> {
|
is NotificationTypes.Room -> {
|
||||||
if (!onlyContainsRemovals) {
|
if (!onlyContainsRemovals) {
|
||||||
log(AppLogTag.NOTIFICATION, "notifying ${it.roomId.value}")
|
log(AppLogTag.NOTIFICATION, "notifying ${it.roomId.value}")
|
||||||
notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
|
notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification.build(androidNotificationBuilder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.summaryNotification?.let {
|
notifications.summaryNotification?.let {
|
||||||
if (notifications.delegates.filterIsInstance<NotificationDelegate.Room>().isNotEmpty() && !onlyContainsRemovals) {
|
if (notifications.delegates.filterIsInstance<NotificationTypes.Room>().isNotEmpty() && !onlyContainsRemovals) {
|
||||||
log(AppLogTag.NOTIFICATION, "notifying summary")
|
log(AppLogTag.NOTIFICATION, "notifying summary")
|
||||||
notificationManager.notify(SUMMARY_NOTIFICATION_ID, it)
|
notificationManager.notify(SUMMARY_NOTIFICATION_ID, it.build(androidNotificationBuilder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NotificationDelegate {
|
data class NotificationState(
|
||||||
data class Room(val notification: Notification, val roomId: RoomId, val summary: String, val messageCount: Int, val isAlerting: Boolean) : NotificationDelegate
|
val allUnread: Map<RoomOverview, List<RoomEvent>>,
|
||||||
data class DismissRoom(val roomId: RoomId) : NotificationDelegate
|
val removedRooms: Set<RoomId>,
|
||||||
|
val roomsWithNewEvents: Set<RoomId>,
|
||||||
|
val newRooms: Set<RoomId>
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun NotificationState.onlyContainsRemovals() = this.removedRooms.isNotEmpty() && this.roomsWithNewEvents.isEmpty()
|
||||||
|
|
||||||
|
sealed interface NotificationTypes {
|
||||||
|
data class Room(
|
||||||
|
val notification: AndroidNotification,
|
||||||
|
val roomId: RoomId,
|
||||||
|
val summary: String,
|
||||||
|
val messageCount: Int,
|
||||||
|
val isAlerting: Boolean
|
||||||
|
) : NotificationTypes
|
||||||
|
|
||||||
|
data class DismissRoom(val roomId: RoomId) : NotificationTypes
|
||||||
}
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
class NotificationStateMapper(
|
||||||
|
private val roomEventsToNotifiableMapper: RoomEventsToNotifiableMapper,
|
||||||
|
private val notificationFactory: NotificationFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun mapToNotifications(state: NotificationState): Notifications {
|
||||||
|
val messageNotifications = createMessageNotifications(state)
|
||||||
|
val roomNotifications = messageNotifications.filterIsInstance<NotificationTypes.Room>()
|
||||||
|
val summaryNotification = maybeCreateSummary(roomNotifications)
|
||||||
|
return Notifications(summaryNotification, messageNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createMessageNotifications(state: NotificationState) = state.allUnread.map { (roomOverview, events) ->
|
||||||
|
val messageEvents = roomEventsToNotifiableMapper.map(events)
|
||||||
|
when (messageEvents.isEmpty()) {
|
||||||
|
true -> NotificationTypes.DismissRoom(roomOverview.roomId)
|
||||||
|
false -> notificationFactory.createMessageNotification(messageEvents, roomOverview, state.roomsWithNewEvents, state.newRooms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeCreateSummary(roomNotifications: List<NotificationTypes.Room>) = when {
|
||||||
|
roomNotifications.isNotEmpty() -> {
|
||||||
|
notificationFactory.createSummary(roomNotifications)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Notifications(val summaryNotification: AndroidNotification?, val delegates: List<NotificationTypes>)
|
|
@ -0,0 +1,53 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.core.whenPOrHigher
|
||||||
|
import app.dapk.st.imageloader.IconLoader
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
|
import app.dapk.st.notifications.AndroidNotificationStyle.Inbox
|
||||||
|
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
class NotificationStyleFactory(
|
||||||
|
private val iconLoader: IconLoader,
|
||||||
|
private val deviceMeta: DeviceMeta,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun summary(notifications: List<NotificationTypes.Room>) = Inbox(
|
||||||
|
lines = notifications
|
||||||
|
.sortedBy { it.notification.whenTimestamp }
|
||||||
|
.map { it.summary },
|
||||||
|
summary = "${notifications.countMessages()} messages from ${notifications.size} chats",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun List<NotificationTypes.Room>.countMessages() = this.sumOf { it.messageCount }
|
||||||
|
|
||||||
|
suspend fun message(events: List<Notifiable>, roomOverview: RoomOverview): AndroidNotificationStyle {
|
||||||
|
return deviceMeta.whenPOrHigher(
|
||||||
|
block = { createMessageStyle(events, roomOverview) },
|
||||||
|
fallback = {
|
||||||
|
val lines = events.map { "${it.author.displayName ?: it.author.id.value}: ${it.content}" }
|
||||||
|
Inbox(lines)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createMessageStyle(events: List<Notifiable>, roomOverview: RoomOverview) = Messaging(
|
||||||
|
Messaging.AndroidPerson(name = "me", key = roomOverview.roomId.value),
|
||||||
|
title = roomOverview.roomName.takeIf { roomOverview.isGroup },
|
||||||
|
isGroup = roomOverview.isGroup,
|
||||||
|
content = events.map { message ->
|
||||||
|
Messaging.AndroidMessage(
|
||||||
|
Messaging.AndroidPerson(
|
||||||
|
name = message.author.displayName ?: message.author.id.value,
|
||||||
|
icon = message.author.avatarUrl?.let { iconLoader.load(it.value) },
|
||||||
|
key = message.author.id.value,
|
||||||
|
),
|
||||||
|
content = message.content,
|
||||||
|
timestamp = message.utcTimestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package app.dapk.st.notifications
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.imageloader.IconLoader
|
import app.dapk.st.imageloader.IconLoader
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
|
@ -24,6 +25,7 @@ class NotificationsModule(
|
||||||
private val workScheduler: WorkScheduler,
|
private val workScheduler: WorkScheduler,
|
||||||
private val intentFactory: IntentFactory,
|
private val intentFactory: IntentFactory,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val deviceMeta: DeviceMeta,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun pushUseCase() = pushService
|
fun pushUseCase() = pushService
|
||||||
|
@ -32,9 +34,23 @@ class NotificationsModule(
|
||||||
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
|
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
|
||||||
fun roomStore() = roomStore
|
fun roomStore() = roomStore
|
||||||
fun notificationsUseCase() = RenderNotificationsUseCase(
|
fun notificationsUseCase() = RenderNotificationsUseCase(
|
||||||
NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context, intentFactory), dispatchers),
|
notificationRenderer = NotificationRenderer(
|
||||||
ObserveUnreadNotificationsUseCaseImpl(roomStore),
|
notificationManager(),
|
||||||
NotificationChannels(notificationManager()),
|
NotificationStateMapper(
|
||||||
|
RoomEventsToNotifiableMapper(),
|
||||||
|
NotificationFactory(
|
||||||
|
context,
|
||||||
|
NotificationStyleFactory(iconLoader, deviceMeta),
|
||||||
|
intentFactory,
|
||||||
|
iconLoader,
|
||||||
|
deviceMeta,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
AndroidNotificationBuilder(context, deviceMeta, AndroidNotificationStyleBuilder()),
|
||||||
|
dispatchers
|
||||||
|
),
|
||||||
|
observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore),
|
||||||
|
notificationChannels = NotificationChannels(notificationManager()),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
|
@ -26,10 +26,12 @@ class RenderNotificationsUseCase(
|
||||||
private suspend fun renderUnreadChange(allUnread: Map<RoomOverview, List<RoomEvent>>, diff: NotificationDiff) {
|
private suspend fun renderUnreadChange(allUnread: Map<RoomOverview, List<RoomEvent>>, diff: NotificationDiff) {
|
||||||
log(NOTIFICATION, "unread changed - render notifications")
|
log(NOTIFICATION, "unread changed - render notifications")
|
||||||
notificationRenderer.render(
|
notificationRenderer.render(
|
||||||
allUnread = allUnread,
|
NotificationState(
|
||||||
removedRooms = diff.removed.keys,
|
allUnread = allUnread,
|
||||||
roomsWithNewEvents = diff.changedOrNew.keys,
|
removedRooms = diff.removed.keys,
|
||||||
newRooms = diff.newRooms,
|
roomsWithNewEvents = diff.changedOrNew.keys,
|
||||||
|
newRooms = diff.newRooms,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
|
|
||||||
|
class RoomEventsToNotifiableMapper {
|
||||||
|
|
||||||
|
fun map(events: List<RoomEvent>): List<Notifiable> {
|
||||||
|
return events.map {
|
||||||
|
when (it) {
|
||||||
|
is RoomEvent.Image -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
||||||
|
is RoomEvent.Message -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
||||||
|
is RoomEvent.Reply -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomEvent.toNotifiableContent(): String = when (this) {
|
||||||
|
is RoomEvent.Image -> "\uD83D\uDCF7"
|
||||||
|
is RoomEvent.Message -> this.content
|
||||||
|
is RoomEvent.Reply -> this.message.toNotifiableContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Notifiable(val content: String, val utcTimestamp: Long, val author: RoomMember)
|
|
@ -0,0 +1,59 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
|
import fake.FakeContext
|
||||||
|
import fake.FakeNotificationBuilder
|
||||||
|
import fake.aFakeMessagingStyle
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Test
|
||||||
|
import test.delegateReturn
|
||||||
|
import test.runExpectTest
|
||||||
|
|
||||||
|
private val A_MESSAGING_STYLE = aFakeMessagingStyle()
|
||||||
|
|
||||||
|
class AndroidNotificationBuilderTest {
|
||||||
|
|
||||||
|
private val fakeContext = FakeContext()
|
||||||
|
private val fakeNotificationBuilder = FakeNotificationBuilder()
|
||||||
|
private val fakeAndroidNotificationStyleBuilder = FakeAndroidNotificationStyleBuilder()
|
||||||
|
|
||||||
|
private val builder = AndroidNotificationBuilder(
|
||||||
|
fakeContext.instance,
|
||||||
|
DeviceMeta(apiVersion = 26),
|
||||||
|
fakeAndroidNotificationStyleBuilder.instance,
|
||||||
|
builderFactory = { _, _, _ -> fakeNotificationBuilder.instance },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `applies all builder options`() = runExpectTest {
|
||||||
|
val notification = anAndroidNotification()
|
||||||
|
fakeAndroidNotificationStyleBuilder.given(notification.messageStyle!!).returns(A_MESSAGING_STYLE)
|
||||||
|
fakeNotificationBuilder.instance.captureExpects {
|
||||||
|
it.setOnlyAlertOnce(!notification.alertMoreThanOnce)
|
||||||
|
it.setAutoCancel(notification.autoCancel)
|
||||||
|
it.setGroupSummary(notification.isGroupSummary)
|
||||||
|
it.setGroup(notification.groupId)
|
||||||
|
it.setStyle(A_MESSAGING_STYLE)
|
||||||
|
it.setContentIntent(notification.contentIntent)
|
||||||
|
it.setShowWhen(true)
|
||||||
|
it.setWhen(notification.whenTimestamp!!)
|
||||||
|
it.setCategory(notification.category)
|
||||||
|
it.setShortcutId(notification.shortcutId)
|
||||||
|
it.setSmallIcon(notification.smallIcon!!)
|
||||||
|
it.setLargeIcon(notification.largeIcon)
|
||||||
|
it.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ignoredResult = builder.build(notification)
|
||||||
|
|
||||||
|
verifyExpects()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeAndroidNotificationStyleBuilder {
|
||||||
|
val instance = mockk<AndroidNotificationStyleBuilder>()
|
||||||
|
|
||||||
|
fun given(style: AndroidNotificationStyle) = every { instance.build(style) }.delegateReturn()
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import fake.FakeInboxStyle
|
||||||
|
import fake.FakeMessagingStyle
|
||||||
|
import fake.FakePersonBuilder
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AndroidNotificationStyleBuilderTest {
|
||||||
|
|
||||||
|
private val fakePersonBuilder = FakePersonBuilder()
|
||||||
|
private val fakeInbox = FakeInboxStyle().also { it.captureInteractions() }
|
||||||
|
private val fakeMessagingStyle = FakeMessagingStyle()
|
||||||
|
|
||||||
|
private val styleBuilder = AndroidNotificationStyleBuilder(
|
||||||
|
personBuilderFactory = { fakePersonBuilder.instance },
|
||||||
|
inboxStyleFactory = { fakeInbox.instance },
|
||||||
|
messagingStyleFactory = {
|
||||||
|
fakeMessagingStyle.user = it
|
||||||
|
fakeMessagingStyle.instance
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an inbox style, when building android style, then returns framework version`() {
|
||||||
|
val input = AndroidNotificationStyle.Inbox(
|
||||||
|
lines = listOf("hello", "world"),
|
||||||
|
summary = "a summary"
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = styleBuilder.build(input)
|
||||||
|
|
||||||
|
result shouldBeEqualTo fakeInbox.instance
|
||||||
|
fakeInbox.lines shouldBeEqualTo input.lines
|
||||||
|
fakeInbox.summary shouldBeEqualTo input.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.os.Build
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.matrix.common.AvatarUrl
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
|
import fake.FakeContext
|
||||||
|
import fixture.NotificationDelegateFixtures.anInboxStyle
|
||||||
|
import fixture.NotificationFixtures.aRoomNotification
|
||||||
|
import fixture.aRoomId
|
||||||
|
import fixture.aRoomOverview
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private val AN_OPEN_APP_INTENT = aPendingIntent()
|
||||||
|
private val AN_OPEN_ROOM_INTENT = aPendingIntent()
|
||||||
|
private val A_NOTIFICATION_STYLE = anInboxStyle()
|
||||||
|
private val A_ROOM_ID = aRoomId()
|
||||||
|
private val A_DM_ROOM_OVERVIEW = aRoomOverview(roomId = A_ROOM_ID, roomAvatarUrl = AvatarUrl("https://a-url.gif"), isGroup = false)
|
||||||
|
private val A_GROUP_ROOM_OVERVIEW = aRoomOverview(roomId = A_ROOM_ID, roomAvatarUrl = AvatarUrl("https://a-url.gif"), isGroup = true)
|
||||||
|
private val A_ROOM_ICON = anIcon()
|
||||||
|
private val LATEST_EVENT = aNotifiable("message three", utcTimestamp = 3)
|
||||||
|
private val EVENTS = listOf(
|
||||||
|
aNotifiable("message one", utcTimestamp = 1),
|
||||||
|
LATEST_EVENT,
|
||||||
|
aNotifiable("message two", utcTimestamp = 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationFactoryTest {
|
||||||
|
|
||||||
|
private val fakeContext = FakeContext()
|
||||||
|
private val fakeNotificationStyleFactory = FakeNotificationStyleFactory()
|
||||||
|
private val fakeIntentFactory = FakeIntentFactory()
|
||||||
|
private val fakeIconLoader = FakeIconLoader()
|
||||||
|
|
||||||
|
private val notificationFactory = NotificationFactory(
|
||||||
|
fakeContext.instance,
|
||||||
|
fakeNotificationStyleFactory.instance,
|
||||||
|
fakeIntentFactory,
|
||||||
|
fakeIconLoader,
|
||||||
|
DeviceMeta(26),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given alerting room notification, when creating summary, then is alerting`() {
|
||||||
|
val notifications = listOf(aRoomNotification(isAlerting = true))
|
||||||
|
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
|
||||||
|
fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle())
|
||||||
|
|
||||||
|
val result = notificationFactory.createSummary(notifications)
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given non alerting room notification, when creating summary, then is alerting`() {
|
||||||
|
val notifications = listOf(aRoomNotification(isAlerting = false))
|
||||||
|
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
|
||||||
|
fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle())
|
||||||
|
|
||||||
|
val result = notificationFactory.createSummary(notifications)
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given new events in a new group room, when creating message, then alerts`() = runTest {
|
||||||
|
givenEventsFor(A_GROUP_ROOM_OVERVIEW)
|
||||||
|
|
||||||
|
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedMessage(
|
||||||
|
shouldAlertMoreThanOnce = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given new events in an existing group room, when creating message, then does not alert`() = runTest {
|
||||||
|
givenEventsFor(A_GROUP_ROOM_OVERVIEW)
|
||||||
|
|
||||||
|
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedMessage(
|
||||||
|
shouldAlertMoreThanOnce = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given new events in a new DM room, when creating message, then alerts`() = runTest {
|
||||||
|
givenEventsFor(A_DM_ROOM_OVERVIEW)
|
||||||
|
|
||||||
|
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedMessage(
|
||||||
|
shouldAlertMoreThanOnce = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given new events in an existing DM room, when creating message, then alerts`() = runTest {
|
||||||
|
givenEventsFor(A_DM_ROOM_OVERVIEW)
|
||||||
|
|
||||||
|
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
|
||||||
|
|
||||||
|
result shouldBeEqualTo expectedMessage(
|
||||||
|
shouldAlertMoreThanOnce = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenEventsFor(roomOverview: RoomOverview) {
|
||||||
|
fakeIntentFactory.givenNotificationOpenMessage(fakeContext.instance, roomOverview.roomId).returns(AN_OPEN_ROOM_INTENT)
|
||||||
|
fakeNotificationStyleFactory.givenMessage(EVENTS.sortedBy { it.utcTimestamp }, roomOverview).returns(A_NOTIFICATION_STYLE)
|
||||||
|
fakeIconLoader.given(roomOverview.roomAvatarUrl!!.value).returns(A_ROOM_ICON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectedMessage(
|
||||||
|
shouldAlertMoreThanOnce: Boolean,
|
||||||
|
) = NotificationTypes.Room(
|
||||||
|
AndroidNotification(
|
||||||
|
channelId = "message",
|
||||||
|
whenTimestamp = LATEST_EVENT.utcTimestamp,
|
||||||
|
groupId = "st",
|
||||||
|
groupAlertBehavior = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.GROUP_ALERT_SUMMARY else null,
|
||||||
|
shortcutId = A_ROOM_ID.value,
|
||||||
|
alertMoreThanOnce = shouldAlertMoreThanOnce,
|
||||||
|
contentIntent = AN_OPEN_ROOM_INTENT,
|
||||||
|
messageStyle = A_NOTIFICATION_STYLE,
|
||||||
|
category = Notification.CATEGORY_MESSAGE,
|
||||||
|
smallIcon = R.drawable.ic_notification_small_icon,
|
||||||
|
largeIcon = A_ROOM_ICON,
|
||||||
|
autoCancel = true
|
||||||
|
),
|
||||||
|
A_ROOM_ID,
|
||||||
|
summary = LATEST_EVENT.content,
|
||||||
|
messageCount = EVENTS.size,
|
||||||
|
isAlerting = shouldAlertMoreThanOnce,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun expectedSummary(shouldAlertMoreThanOnce: Boolean) = AndroidNotification(
|
||||||
|
channelId = "message",
|
||||||
|
messageStyle = A_NOTIFICATION_STYLE,
|
||||||
|
alertMoreThanOnce = shouldAlertMoreThanOnce,
|
||||||
|
smallIcon = R.drawable.ic_notification_small_icon,
|
||||||
|
contentIntent = AN_OPEN_APP_INTENT,
|
||||||
|
groupId = "st",
|
||||||
|
isGroupSummary = true,
|
||||||
|
autoCancel = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aPendingIntent() = mockk<PendingIntent>()
|
|
@ -1,59 +1,76 @@
|
||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import fixture.NotificationFixtures.aNotifications
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import fake.FakeNotificationFactory
|
import fake.FakeNotificationFactory
|
||||||
import fake.FakeNotificationManager
|
import fake.FakeNotificationManager
|
||||||
import fake.aFakeNotification
|
import fake.aFakeNotification
|
||||||
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
|
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
|
||||||
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
|
import fixture.NotificationFixtures.aNotifications
|
||||||
|
import fixture.NotificationFixtures.aRoomNotification
|
||||||
import fixture.aRoomId
|
import fixture.aRoomId
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import test.delegateReturn
|
||||||
import test.expect
|
import test.expect
|
||||||
import test.runExpectTest
|
import test.runExpectTest
|
||||||
|
|
||||||
private const val SUMMARY_ID = 101
|
private const val SUMMARY_ID = 101
|
||||||
private const val ROOM_MESSAGE_ID = 100
|
private const val ROOM_MESSAGE_ID = 100
|
||||||
private val A_SUMMARY_NOTIFICATION = aFakeNotification()
|
private val A_SUMMARY_ANDROID_NOTIFICATION = anAndroidNotification(isGroupSummary = true)
|
||||||
|
private val A_NOTIFICATION = aFakeNotification()
|
||||||
|
|
||||||
|
class FakeAndroidNotificationBuilder {
|
||||||
|
val instance = mockk<AndroidNotificationBuilder>()
|
||||||
|
|
||||||
|
fun given(notification: AndroidNotification) = every { instance.build(notification) }.delegateReturn()
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationRendererTest {
|
class NotificationRendererTest {
|
||||||
|
|
||||||
private val fakeNotificationManager = FakeNotificationManager()
|
private val fakeNotificationManager = FakeNotificationManager()
|
||||||
private val fakeNotificationFactory = FakeNotificationFactory()
|
private val fakeNotificationFactory = FakeNotificationFactory()
|
||||||
|
private val fakeAndroidNotificationBuilder = FakeAndroidNotificationBuilder()
|
||||||
|
|
||||||
private val notificationRenderer = NotificationRenderer(
|
private val notificationRenderer = NotificationRenderer(
|
||||||
fakeNotificationManager.instance,
|
fakeNotificationManager.instance,
|
||||||
fakeNotificationFactory.instance,
|
fakeNotificationFactory.instance,
|
||||||
|
fakeAndroidNotificationBuilder.instance,
|
||||||
aCoroutineDispatchers()
|
aCoroutineDispatchers()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest {
|
fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest {
|
||||||
val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2"))
|
val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2"))
|
||||||
fakeNotificationFactory.instance.expect { it.createNotifications(emptyMap(), emptySet(), emptySet()) }
|
fakeNotificationFactory.instance.expect { it.mapToNotifications(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet())) }
|
||||||
fakeNotificationManager.instance.expectUnit {
|
fakeNotificationManager.instance.expectUnit {
|
||||||
removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) }
|
removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) }
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationRenderer.render(emptyMap(), removedRooms, emptySet(), emptySet())
|
notificationRenderer.render(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet()))
|
||||||
|
|
||||||
verifyExpects()
|
verifyExpects()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given summary notification is not created, when rendering, then cancels summary notification`() = runExpectTest {
|
fun `given summary notification is not created, when rendering, then cancels summary notification`() = runExpectTest {
|
||||||
fakeNotificationFactory.givenNotifications(emptyMap(), emptySet(), emptySet()).returns(aNotifications(summaryNotification = null))
|
fakeNotificationFactory.givenNotifications(aNotificationState()).returns(aNotifications(summaryNotification = null))
|
||||||
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
|
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
|
||||||
|
|
||||||
notificationRenderer.render(emptyMap(), emptySet(), emptySet(), emptySet())
|
notificationRenderer.render(NotificationState(emptyMap(), emptySet(), emptySet(), emptySet()))
|
||||||
|
|
||||||
verifyExpects()
|
verifyExpects()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given update is only removals, when rendering, then only renders room dismiss`() = runExpectTest {
|
fun `given update is only removals, when rendering, then only renders room dismiss`() = runExpectTest {
|
||||||
fakeNotificationFactory.givenNotifications(emptyMap(), emptySet(), emptySet()).returns(aNotifications(summaryNotification = null))
|
fakeNotificationFactory.givenNotifications(aNotificationState()).returns(aNotifications(summaryNotification = null))
|
||||||
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
|
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
|
||||||
|
|
||||||
notificationRenderer.render(emptyMap(), emptySet(), emptySet(), emptySet())
|
notificationRenderer.render(NotificationState(emptyMap(), emptySet(), emptySet(), emptySet()))
|
||||||
|
|
||||||
verifyExpects()
|
verifyExpects()
|
||||||
}
|
}
|
||||||
|
@ -62,24 +79,26 @@ class NotificationRendererTest {
|
||||||
fun `given rooms with events, when rendering, then notifies summary and new rooms`() = runExpectTest {
|
fun `given rooms with events, when rendering, then notifies summary and new rooms`() = runExpectTest {
|
||||||
val roomNotification = aRoomNotification()
|
val roomNotification = aRoomNotification()
|
||||||
val roomsWithNewEvents = setOf(roomNotification.roomId)
|
val roomsWithNewEvents = setOf(roomNotification.roomId)
|
||||||
fakeNotificationFactory.givenNotifications(emptyMap(), roomsWithNewEvents, emptySet()).returns(
|
|
||||||
aNotifications(summaryNotification = A_SUMMARY_NOTIFICATION, delegates = listOf(roomNotification))
|
|
||||||
)
|
|
||||||
fakeNotificationManager.instance.expectUnit { it.notify(SUMMARY_ID, A_SUMMARY_NOTIFICATION) }
|
|
||||||
fakeNotificationManager.instance.expectUnit { it.notify(roomNotification.roomId.value, ROOM_MESSAGE_ID, roomNotification.notification) }
|
|
||||||
|
|
||||||
notificationRenderer.render(emptyMap(), emptySet(), roomsWithNewEvents, emptySet())
|
fakeAndroidNotificationBuilder.given(roomNotification.notification).returns(A_NOTIFICATION)
|
||||||
|
fakeAndroidNotificationBuilder.given(A_SUMMARY_ANDROID_NOTIFICATION).returns(A_NOTIFICATION)
|
||||||
|
|
||||||
|
fakeNotificationFactory.givenNotifications(aNotificationState(roomsWithNewEvents = roomsWithNewEvents)).returns(
|
||||||
|
aNotifications(summaryNotification = A_SUMMARY_ANDROID_NOTIFICATION, delegates = listOf(roomNotification))
|
||||||
|
)
|
||||||
|
fakeNotificationManager.instance.expectUnit { it.notify(SUMMARY_ID, A_NOTIFICATION) }
|
||||||
|
fakeNotificationManager.instance.expectUnit { it.notify(roomNotification.roomId.value, ROOM_MESSAGE_ID, A_NOTIFICATION) }
|
||||||
|
|
||||||
|
notificationRenderer.render(NotificationState(emptyMap(), emptySet(), roomsWithNewEvents, emptySet()))
|
||||||
|
|
||||||
verifyExpects()
|
verifyExpects()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aRoomNotification() = NotificationDelegate.Room(
|
|
||||||
aFakeNotification(),
|
|
||||||
aRoomId(),
|
|
||||||
"a summary line",
|
|
||||||
messageCount = 1,
|
|
||||||
isAlerting = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aNotificationState(
|
||||||
|
allUnread: Map<RoomOverview, List<RoomEvent>> = emptyMap(),
|
||||||
|
removedRooms: Set<RoomId> = emptySet(),
|
||||||
|
roomsWithNewEvents: Set<RoomId> = emptySet(),
|
||||||
|
newRooms: Set<RoomId> = emptySet(),
|
||||||
|
) = NotificationState(allUnread, removedRooms, roomsWithNewEvents, newRooms)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.navigator.IntentFactory
|
||||||
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
|
import fixture.NotificationFixtures.aDismissRoomNotification
|
||||||
|
import fixture.NotificationFixtures.aRoomNotification
|
||||||
|
import fixture.aRoomMessageEvent
|
||||||
|
import fixture.aRoomOverview
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import test.delegateReturn
|
||||||
|
|
||||||
|
private val A_SUMMARY_NOTIFICATION = anAndroidNotification()
|
||||||
|
private val A_ROOM_OVERVIEW = aRoomOverview()
|
||||||
|
|
||||||
|
class NotificationStateMapperTest {
|
||||||
|
|
||||||
|
private val fakeRoomEventsToNotifiableMapper = FakeRoomEventsToNotifiableMapper()
|
||||||
|
private val fakeNotificationFactory = FakeNotificationFactory()
|
||||||
|
|
||||||
|
private val factory = NotificationStateMapper(
|
||||||
|
fakeRoomEventsToNotifiableMapper.instance,
|
||||||
|
fakeNotificationFactory.instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no room message events, when mapping notifications, then creates doesn't create summary and dismisses rooms`() = runTest {
|
||||||
|
val notificationState = aNotificationState(allUnread = mapOf(A_ROOM_OVERVIEW to listOf()))
|
||||||
|
fakeRoomEventsToNotifiableMapper.given(emptyList()).returns(emptyList())
|
||||||
|
|
||||||
|
val result = factory.mapToNotifications(notificationState)
|
||||||
|
|
||||||
|
result shouldBeEqualTo Notifications(
|
||||||
|
summaryNotification = null,
|
||||||
|
delegates = listOf(aDismissRoomNotification(A_ROOM_OVERVIEW.roomId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given room message events, when mapping notifications, then creates summary and message notifications`() = runTest {
|
||||||
|
val notificationState = aNotificationState(allUnread = mapOf(aRoomOverview() to listOf(aRoomMessageEvent())))
|
||||||
|
val expectedNotification = givenCreatesNotification(notificationState, aRoomNotification())
|
||||||
|
fakeNotificationFactory.givenCreateSummary(listOf(expectedNotification)).returns(A_SUMMARY_NOTIFICATION)
|
||||||
|
|
||||||
|
val result = factory.mapToNotifications(notificationState)
|
||||||
|
|
||||||
|
result shouldBeEqualTo Notifications(
|
||||||
|
summaryNotification = A_SUMMARY_NOTIFICATION,
|
||||||
|
delegates = listOf(expectedNotification)
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// val allUnread = listOf(aRoomMessageEvent())
|
||||||
|
// val value = listOf(aNotifiable())
|
||||||
|
// fakeRoomEventsToNotifiableMapper.given(allUnread).returns(value)
|
||||||
|
//
|
||||||
|
// fakeIntentFactory.notificationOpenApp()
|
||||||
|
// fakeIntentFactory.notificationOpenMessage()
|
||||||
|
//
|
||||||
|
// fakeNotificationStyleFactory.givenMessage(value, aRoomOverview()).returns(aMessagingStyle())
|
||||||
|
// fakeNotificationStyleFactory.givenSummary(listOf()).returns(anInboxStyle())
|
||||||
|
//
|
||||||
|
// val result = factory.mapToNotifications(
|
||||||
|
// allUnread = mapOf(
|
||||||
|
// aRoomOverview() to allUnread
|
||||||
|
// ),
|
||||||
|
// roomsWithNewEvents = setOf(),
|
||||||
|
// newRooms = setOf()
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// result shouldBeEqualTo Notifications(
|
||||||
|
// summaryNotification = anAndroidNotification(),
|
||||||
|
// delegates = listOf(
|
||||||
|
// NotificationTypes.Room(
|
||||||
|
// anAndroidNotification(),
|
||||||
|
// aRoomId(),
|
||||||
|
// summary = "a summary",
|
||||||
|
// messageCount = 1,
|
||||||
|
// isAlerting = false
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenCreatesNotification(state: NotificationState, result: NotificationTypes.Room): NotificationTypes.Room {
|
||||||
|
state.allUnread.map { (roomOverview, events) ->
|
||||||
|
val value = listOf(aNotifiable())
|
||||||
|
fakeRoomEventsToNotifiableMapper.given(events).returns(value)
|
||||||
|
fakeNotificationFactory.givenCreateMessage(
|
||||||
|
value,
|
||||||
|
roomOverview,
|
||||||
|
state.roomsWithNewEvents,
|
||||||
|
state.newRooms
|
||||||
|
).returns(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIntentFactory : IntentFactory by mockk() {
|
||||||
|
fun givenNotificationOpenApp(context: Context) = every { notificationOpenApp(context) }.delegateReturn()
|
||||||
|
fun givenNotificationOpenMessage(context: Context, roomId: RoomId) = every { notificationOpenMessage(context, roomId) }.delegateReturn()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeNotificationStyleFactory {
|
||||||
|
val instance = mockk<NotificationStyleFactory>()
|
||||||
|
|
||||||
|
fun givenMessage(events: List<Notifiable>, roomOverview: RoomOverview) = coEvery {
|
||||||
|
instance.message(events, roomOverview)
|
||||||
|
}.delegateReturn()
|
||||||
|
|
||||||
|
fun givenSummary(notifications: List<NotificationTypes.Room>) = every { instance.summary(notifications) }.delegateReturn()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeRoomEventsToNotifiableMapper {
|
||||||
|
val instance = mockk<RoomEventsToNotifiableMapper>()
|
||||||
|
|
||||||
|
fun given(events: List<RoomEvent>) = every { instance.map(events) }.delegateReturn()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeNotificationFactory {
|
||||||
|
val instance = mockk<NotificationFactory>()
|
||||||
|
|
||||||
|
fun givenCreateMessage(
|
||||||
|
events: List<Notifiable>,
|
||||||
|
roomOverview: RoomOverview,
|
||||||
|
roomsWithNewEvents: Set<RoomId>,
|
||||||
|
newRooms: Set<RoomId>
|
||||||
|
) = coEvery { instance.createMessageNotification(events, roomOverview, roomsWithNewEvents, newRooms) }.delegateReturn()
|
||||||
|
|
||||||
|
fun givenCreateSummary(roomNotifications: List<NotificationTypes.Room>) = every { instance.createSummary(roomNotifications) }.delegateReturn()
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.imageloader.IconLoader
|
||||||
|
import app.dapk.st.matrix.common.AvatarUrl
|
||||||
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
import app.dapk.st.notifications.AndroidNotificationStyle.Inbox
|
||||||
|
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging
|
||||||
|
import fixture.NotificationDelegateFixtures.anAndroidPerson
|
||||||
|
import fixture.NotificationFixtures.aRoomNotification
|
||||||
|
import fixture.aRoomMember
|
||||||
|
import fixture.aRoomOverview
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import test.delegateReturn
|
||||||
|
|
||||||
|
private val A_GROUP_ROOM_OVERVIEW = aRoomOverview(roomName = "my awesome room", isGroup = true)
|
||||||
|
|
||||||
|
class NotificationStyleFactoryTest {
|
||||||
|
|
||||||
|
private val fakeIconLoader = FakeIconLoader()
|
||||||
|
|
||||||
|
private val styleFactory = NotificationStyleFactory(
|
||||||
|
fakeIconLoader,
|
||||||
|
DeviceMeta(28),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when creating summary style, then creates android framework inbox style`() {
|
||||||
|
val result = styleFactory.summary(
|
||||||
|
listOf(
|
||||||
|
aRoomNotification(summary = "room 1 summary", messageCount = 10),
|
||||||
|
aRoomNotification(summary = "room 2 summary", messageCount = 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result shouldBeEqualTo Inbox(
|
||||||
|
lines = listOf("room 1 summary", "room 2 summary"),
|
||||||
|
summary = "11 messages from 2 chats"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when creating message style, then creates android framework messaging style`() = runTest {
|
||||||
|
val aMessage = aNotifiable(author = aRoomMember(displayName = "a display name", avatarUrl = AvatarUrl("a-url")))
|
||||||
|
val authorIcon = anIcon()
|
||||||
|
fakeIconLoader.given(aMessage.author.avatarUrl!!.value).returns(authorIcon)
|
||||||
|
|
||||||
|
val result = styleFactory.message(listOf(aMessage), A_GROUP_ROOM_OVERVIEW)
|
||||||
|
|
||||||
|
result shouldBeEqualTo Messaging(
|
||||||
|
person = Messaging.AndroidPerson(name = "me", key = A_GROUP_ROOM_OVERVIEW.roomId.value, icon = null),
|
||||||
|
title = A_GROUP_ROOM_OVERVIEW.roomName,
|
||||||
|
isGroup = true,
|
||||||
|
content = listOf(aMessage.toAndroidMessage(authorIcon))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Notifiable.toAndroidMessage(expectedAuthorIcon: Icon) = Messaging.AndroidMessage(
|
||||||
|
anAndroidPerson(
|
||||||
|
name = author.displayName!!,
|
||||||
|
key = author.id.value,
|
||||||
|
icon = expectedAuthorIcon
|
||||||
|
),
|
||||||
|
content = content,
|
||||||
|
timestamp = utcTimestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aNotifiable(
|
||||||
|
content: String = "notifiable content",
|
||||||
|
utcTimestamp: Long = 1000,
|
||||||
|
author: RoomMember = aRoomMember()
|
||||||
|
) = Notifiable(content, utcTimestamp, author)
|
||||||
|
|
||||||
|
class FakeIconLoader : IconLoader by mockk() {
|
||||||
|
fun given(url: String) = coEvery { load(url) }.delegateReturn()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIcon {
|
||||||
|
val instance = mockk<Icon>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anIcon() = FakeIcon().instance
|
|
@ -12,10 +12,10 @@ import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>()
|
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>()
|
||||||
val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello")
|
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello")
|
||||||
val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world")
|
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world")
|
||||||
val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
|
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
|
||||||
val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
|
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
|
||||||
|
|
||||||
class ObserveUnreadRenderNotificationsUseCaseTest {
|
class ObserveUnreadRenderNotificationsUseCaseTest {
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class RenderNotificationsUseCaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
|
fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
|
||||||
fakeNotificationRenderer.instance.expect { it.render(any(), any(), any(), any()) }
|
fakeNotificationRenderer.instance.expect { it.render(any()) }
|
||||||
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
||||||
|
|
||||||
renderNotificationsUseCase.listenForNotificationChanges()
|
renderNotificationsUseCase.listenForNotificationChanges()
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import fixture.aRoomImageMessageEvent
|
||||||
|
import fixture.aRoomMessageEvent
|
||||||
|
import fixture.aRoomReplyMessageEvent
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomEventsToNotifiableMapperTest {
|
||||||
|
|
||||||
|
private val mapper = RoomEventsToNotifiableMapper()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given message event, when mapping, then uses original content`() {
|
||||||
|
val event = aRoomMessageEvent()
|
||||||
|
|
||||||
|
val result = mapper.map(listOf(event))
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(
|
||||||
|
Notifiable(
|
||||||
|
content = event.content,
|
||||||
|
utcTimestamp = event.utcTimestamp,
|
||||||
|
author = event.author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given image event, when mapping, then replaces content with camera emoji`() {
|
||||||
|
val event = aRoomImageMessageEvent()
|
||||||
|
|
||||||
|
val result = mapper.map(listOf(event))
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(
|
||||||
|
Notifiable(
|
||||||
|
content = "📷",
|
||||||
|
utcTimestamp = event.utcTimestamp,
|
||||||
|
author = event.author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reply event with message, when mapping, then uses message for content`() {
|
||||||
|
val reply = aRoomMessageEvent(utcTimestamp = -1, content = "hello")
|
||||||
|
val event = aRoomReplyMessageEvent(reply, replyingTo = aRoomImageMessageEvent(utcTimestamp = -1))
|
||||||
|
|
||||||
|
val result = mapper.map(listOf(event))
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(
|
||||||
|
Notifiable(
|
||||||
|
content = reply.content,
|
||||||
|
utcTimestamp = event.utcTimestamp,
|
||||||
|
author = event.author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reply event with image, when mapping, then uses camera emoji for content`() {
|
||||||
|
val event = aRoomReplyMessageEvent(aRoomImageMessageEvent(utcTimestamp = -1))
|
||||||
|
|
||||||
|
val result = mapper.map(listOf(event))
|
||||||
|
|
||||||
|
result shouldBeEqualTo listOf(
|
||||||
|
Notifiable(
|
||||||
|
content = "📷",
|
||||||
|
utcTimestamp = event.utcTimestamp,
|
||||||
|
author = event.author
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,15 @@
|
||||||
package fake
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.notifications.NotificationState
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.notifications.NotificationStateMapper
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.notifications.NotificationFactory
|
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateReturn
|
import test.delegateReturn
|
||||||
|
|
||||||
class FakeNotificationFactory {
|
class FakeNotificationFactory {
|
||||||
|
|
||||||
val instance = mockk<NotificationFactory>()
|
val instance = mockk<NotificationStateMapper>()
|
||||||
|
|
||||||
fun givenNotifications(allUnread: Map<RoomOverview, List<RoomEvent>>, roomsWithNewEvents: Set<RoomId>, newRooms: Set<RoomId>) = coEvery {
|
fun givenNotifications(state: NotificationState) = coEvery { instance.mapToNotifications(state) }.delegateReturn()
|
||||||
instance.createNotifications(allUnread, roomsWithNewEvents, newRooms)
|
|
||||||
}.delegateReturn()
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package fake
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.notifications.NotificationRenderer
|
import app.dapk.st.notifications.NotificationRenderer
|
||||||
|
import app.dapk.st.notifications.NotificationState
|
||||||
import app.dapk.st.notifications.UnreadNotifications
|
import app.dapk.st.notifications.UnreadNotifications
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -12,10 +13,12 @@ class FakeNotificationRenderer {
|
||||||
unreadNotifications.forEach { unread ->
|
unreadNotifications.forEach { unread ->
|
||||||
coVerify {
|
coVerify {
|
||||||
instance.render(
|
instance.render(
|
||||||
allUnread = unread.first,
|
NotificationState(
|
||||||
removedRooms = unread.second.removed.keys,
|
allUnread = unread.first,
|
||||||
roomsWithNewEvents = unread.second.changedOrNew.keys,
|
removedRooms = unread.second.removed.keys,
|
||||||
newRooms = unread.second.newRooms,
|
roomsWithNewEvents = unread.second.changedOrNew.keys,
|
||||||
|
newRooms = unread.second.newRooms,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package fixture
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import app.dapk.st.notifications.AndroidNotification
|
||||||
|
import app.dapk.st.notifications.AndroidNotificationStyle
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
object NotificationDelegateFixtures {
|
||||||
|
|
||||||
|
fun anAndroidNotification(
|
||||||
|
channelId: String = "a channel id",
|
||||||
|
whenTimestamp: Long? = 10000,
|
||||||
|
isGroupSummary: Boolean = false,
|
||||||
|
groupId: String? = "group id",
|
||||||
|
groupAlertBehavior: Int? = 5,
|
||||||
|
shortcutId: String? = "shortcut id",
|
||||||
|
alertMoreThanOnce: Boolean = false,
|
||||||
|
contentIntent: PendingIntent? = mockk(),
|
||||||
|
messageStyle: AndroidNotificationStyle? = aMessagingStyle(),
|
||||||
|
category: String? = "a category",
|
||||||
|
smallIcon: Int? = 500,
|
||||||
|
largeIcon: Icon? = mockk(),
|
||||||
|
autoCancel: Boolean = true,
|
||||||
|
) = AndroidNotification(
|
||||||
|
channelId = channelId,
|
||||||
|
whenTimestamp = whenTimestamp,
|
||||||
|
isGroupSummary = isGroupSummary,
|
||||||
|
groupId = groupId,
|
||||||
|
groupAlertBehavior = groupAlertBehavior,
|
||||||
|
shortcutId = shortcutId,
|
||||||
|
alertMoreThanOnce = alertMoreThanOnce,
|
||||||
|
contentIntent = contentIntent,
|
||||||
|
messageStyle = messageStyle,
|
||||||
|
category = category,
|
||||||
|
smallIcon = smallIcon,
|
||||||
|
largeIcon = largeIcon,
|
||||||
|
autoCancel = autoCancel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun aMessagingStyle() = AndroidNotificationStyle.Messaging(
|
||||||
|
anAndroidPerson(),
|
||||||
|
title = null,
|
||||||
|
isGroup = false,
|
||||||
|
content = listOf(
|
||||||
|
AndroidNotificationStyle.Messaging.AndroidMessage(
|
||||||
|
anAndroidPerson(), content = "message content",
|
||||||
|
timestamp = 1000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anInboxStyle() = AndroidNotificationStyle.Inbox(
|
||||||
|
lines = listOf("first line"),
|
||||||
|
summary = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anAndroidPerson(
|
||||||
|
name: String = "a name",
|
||||||
|
key: String = "a unique key",
|
||||||
|
icon: Icon? = null,
|
||||||
|
) = AndroidNotificationStyle.Messaging.AndroidPerson(name, key, icon)
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,32 @@
|
||||||
package fixture
|
package fixture
|
||||||
|
|
||||||
import android.app.Notification
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.notifications.NotificationDelegate
|
import app.dapk.st.notifications.AndroidNotification
|
||||||
|
import app.dapk.st.notifications.NotificationTypes
|
||||||
import app.dapk.st.notifications.Notifications
|
import app.dapk.st.notifications.Notifications
|
||||||
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
|
|
||||||
object NotificationFixtures {
|
object NotificationFixtures {
|
||||||
|
|
||||||
fun aNotifications(
|
fun aNotifications(
|
||||||
summaryNotification: Notification? = null,
|
summaryNotification: AndroidNotification? = null,
|
||||||
delegates: List<NotificationDelegate> = emptyList(),
|
delegates: List<NotificationTypes> = emptyList(),
|
||||||
) = Notifications(summaryNotification, delegates)
|
) = Notifications(summaryNotification, delegates)
|
||||||
|
|
||||||
|
fun aRoomNotification(
|
||||||
|
summary: String = "a summary line",
|
||||||
|
messageCount: Int = 1,
|
||||||
|
isAlerting: Boolean = false,
|
||||||
|
) = NotificationTypes.Room(
|
||||||
|
anAndroidNotification(),
|
||||||
|
aRoomId(),
|
||||||
|
summary = summary,
|
||||||
|
messageCount = messageCount,
|
||||||
|
isAlerting = isAlerting
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aDismissRoomNotification(
|
||||||
|
roomId: RoomId = aRoomId()
|
||||||
|
) = NotificationTypes.DismissRoom(roomId)
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue