Merge branch 'main' into dependabot/gradle/com.google.accompanist-accompanist-systemuicontroller-0.24.13-rc

This commit is contained in:
Adam Brown 2022-07-28 19:51:16 +01:00 committed by GitHub
commit 14fc762a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 167 additions and 48 deletions

View File

@ -136,7 +136,7 @@ ext.kotlinTest = { dependencies ->
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
}
ext.kotlinFixtures = { dependencies ->

View File

@ -34,6 +34,8 @@ class AndroidNotificationStyleBuilder(
.setKey(person.key)
.build()
).also { style ->
style.conversationTitle = title
style.isGroupConversation = isGroup
content.forEach {
val sender = personBuilderFactory()
.setName(it.sender.name)

View File

@ -1,10 +1,15 @@
package app.dapk.st.notifications
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.os.Build
private const val channelId = "message"
const val DIRECT_CHANNEL_ID = "direct_channel_id"
const val GROUP_CHANNEL_ID = "group_channel_id"
const val SUMMARY_CHANNEL_ID = "summary_channel_id"
private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group"
class NotificationChannels(
private val notificationManager: NotificationManager
@ -12,13 +17,43 @@ class NotificationChannels(
fun initChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(channelId) == null) {
notificationManager.createNotificationChannelGroup(NotificationChannelGroup(CHATS_NOTIFICATION_GROUP_ID, "Chats"))
if (notificationManager.getNotificationChannel(DIRECT_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
channelId,
"messages",
DIRECT_CHANNEL_ID,
"Direct notifications",
NotificationManager.IMPORTANCE_HIGH,
)
).also {
it.enableVibration(true)
it.enableLights(true)
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
if (notificationManager.getNotificationChannel(GROUP_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
GROUP_CHANNEL_ID,
"Group notifications",
NotificationManager.IMPORTANCE_HIGH,
).also {
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
if (notificationManager.getNotificationChannel(SUMMARY_CHANNEL_ID) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
SUMMARY_CHANNEL_ID,
"Other notifications",
NotificationManager.IMPORTANCE_DEFAULT,
).also {
it.group = CHATS_NOTIFICATION_GROUP_ID
}
)
}
}

View File

@ -10,7 +10,6 @@ import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.navigator.IntentFactory
private const val GROUP_ID = "st"
private const val channelId = "message"
class NotificationFactory(
private val context: Context,
@ -35,17 +34,18 @@ class NotificationFactory(
else -> newRooms.contains(roomOverview.roomId)
}
val last = sortedEvents.last()
return NotificationTypes.Room(
AndroidNotification(
channelId = channelId,
whenTimestamp = sortedEvents.last().utcTimestamp,
channelId = SUMMARY_CHANNEL_ID,
whenTimestamp = last.utcTimestamp,
groupId = GROUP_ID,
groupAlertBehavior = deviceMeta.whenPOrHigher(
block = { Notification.GROUP_ALERT_SUMMARY },
fallback = { null }
),
shortcutId = roomOverview.roomId.value,
alertMoreThanOnce = shouldAlertMoreThanOnce,
alertMoreThanOnce = false,
contentIntent = openRoomIntent,
messageStyle = messageStyle,
category = Notification.CATEGORY_MESSAGE,
@ -54,9 +54,13 @@ class NotificationFactory(
autoCancel = true
),
roomId = roomOverview.roomId,
summary = sortedEvents.last().content,
summary = last.content,
messageCount = sortedEvents.size,
isAlerting = shouldAlertMoreThanOnce
isAlerting = shouldAlertMoreThanOnce,
summaryChannelId = when {
roomOverview.isDm() -> DIRECT_CHANNEL_ID
else -> GROUP_CHANNEL_ID
}
)
}
@ -64,7 +68,7 @@ class NotificationFactory(
val summaryInboxStyle = notificationStyleFactory.summary(notifications)
val openAppIntent = intentFactory.notificationOpenApp(context)
return AndroidNotification(
channelId = channelId,
channelId = notifications.mostRecent().summaryChannelId,
messageStyle = summaryInboxStyle,
alertMoreThanOnce = notifications.any { it.isAlerting },
smallIcon = R.drawable.ic_notification_small_icon,
@ -75,8 +79,11 @@ class NotificationFactory(
fallback = { null }
),
isGroupSummary = true,
category = Notification.CATEGORY_MESSAGE,
)
}
}
private fun List<NotificationTypes.Room>.mostRecent() = this.sortedBy { it.notification.whenTimestamp }.first()
private fun RoomOverview.isDm() = !this.isGroup

View File

@ -68,7 +68,8 @@ sealed interface NotificationTypes {
val roomId: RoomId,
val summary: String,
val messageCount: Int,
val isAlerting: Boolean
val isAlerting: Boolean,
val summaryChannelId: String,
) : NotificationTypes
data class DismissRoom(val roomId: RoomId) : NotificationTypes

View File

@ -16,7 +16,14 @@ class NotificationStateMapper(
val messageEvents = roomEventsToNotifiableMapper.map(events)
when (messageEvents.isEmpty()) {
true -> NotificationTypes.DismissRoom(roomOverview.roomId)
false -> notificationFactory.createMessageNotification(messageEvents, roomOverview, state.roomsWithNewEvents, state.newRooms)
false -> {
notificationFactory.createMessageNotification(
events = messageEvents,
roomOverview = roomOverview,
roomsWithNewEvents = state.roomsWithNewEvents,
newRooms = state.newRooms
)
}
}
}

View File

@ -34,10 +34,12 @@ private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifi
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
false
}
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
false
}
else -> true
}
}
@ -45,29 +47,48 @@ private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifi
}
private fun Flow<Map<RoomOverview, List<RoomEvent>>>.mapWithDiff(): Flow<Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>> {
val previousUnreadEvents = mutableMapOf<RoomId, List<EventId>>()
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
return this.map { each ->
val allUnreadIds = each.toIds()
val allUnreadIds = each.toTimestampedIds()
val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents)
previousUnreadEvents.clearAndPutAll(allUnreadIds)
each to notificationDiff
}
}
private fun calculateDiff(allUnread: Map<RoomId, List<EventId>>, previousUnread: Map<RoomId, List<EventId>>?): NotificationDiff {
private fun calculateDiff(allUnread: Map<RoomId, List<TimestampedEventId>>, previousUnread: Map<RoomId, List<TimestampedEventId>>?): NotificationDiff {
val previousLatestEventTimestamps = previousUnread.toLatestTimestamps()
val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys
val unchanged = previousUnread?.filter { allUnread.containsKey(it.key) && it.value == allUnread[it.key] } ?: emptyMap()
val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }
val unchanged = previousUnread?.filter {
allUnread.containsKey(it.key) && (it.value == allUnread[it.key])
} ?: emptyMap()
val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) ->
val isChangedRoom = !newRooms.contains(key)
if (isChangedRoom) {
val latest = previousLatestEventTimestamps[key] ?: 0L
value.filter {
val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false)
!isExistingEvent && it.second > latest
}
} else {
value
}
}.filter { it.value.isNotEmpty() }
val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap()
return NotificationDiff(unchanged, changedOrNew, removed, newRooms)
return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms)
}
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId }
private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap()
private fun Map<RoomOverview, List<RoomEvent>>.toIds() = this
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
private fun Map<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this
.mapValues { it.value.toEventIds() }
.mapKeys { it.key.roomId }
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
data class NotificationDiff(
@ -76,3 +97,5 @@ data class NotificationDiff(
val removed: Map<RoomId, List<EventId>>,
val newRooms: Set<RoomId>
)
typealias TimestampedEventId = Pair<EventId, Long>

View File

@ -1,30 +1,25 @@
package app.dapk.st.notifications
import app.dapk.st.core.AppLogTag.NOTIFICATION
import app.dapk.st.core.log
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
class RenderNotificationsUseCase(
private val notificationRenderer: NotificationRenderer,
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase,
notificationChannels: NotificationChannels,
private val notificationChannels: NotificationChannels,
) {
init {
notificationChannels.initChannels()
}
suspend fun listenForNotificationChanges() {
observeRenderableUnreadEventsUseCase()
.onStart { notificationChannels.initChannels() }
.onEach { (each, diff) -> renderUnreadChange(each, diff) }
.collect()
}
private suspend fun renderUnreadChange(allUnread: Map<RoomOverview, List<RoomEvent>>, diff: NotificationDiff) {
log(NOTIFICATION, "unread changed - render notifications")
notificationRenderer.render(
NotificationState(
allUnread = allUnread,

View File

@ -7,6 +7,7 @@ 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.anAndroidNotification
import fixture.NotificationDelegateFixtures.anInboxStyle
import fixture.NotificationFixtures.aRoomNotification
import fixture.aRoomId
@ -16,6 +17,7 @@ import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private const val A_CHANNEL_ID = "a channel id"
private val AN_OPEN_APP_INTENT = aPendingIntent()
private val AN_OPEN_ROOM_INTENT = aPendingIntent()
private val A_NOTIFICATION_STYLE = anInboxStyle()
@ -30,7 +32,6 @@ private val EVENTS = listOf(
aNotifiable("message two", utcTimestamp = 2),
)
class NotificationFactoryTest {
private val fakeContext = FakeContext()
@ -48,24 +49,34 @@ class NotificationFactoryTest {
@Test
fun `given alerting room notification, when creating summary, then is alerting`() {
val notifications = listOf(aRoomNotification(isAlerting = true))
val notifications = listOf(
aRoomNotification(
summaryChannelId = A_CHANNEL_ID,
notification = anAndroidNotification(channelId = A_CHANNEL_ID), 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)
result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = true)
}
@Test
fun `given non alerting room notification, when creating summary, then is alerting`() {
val notifications = listOf(aRoomNotification(isAlerting = false))
val notifications = listOf(
aRoomNotification(
summaryChannelId = A_CHANNEL_ID,
notification = anAndroidNotification(channelId = A_CHANNEL_ID), 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)
result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = false)
}
@Test
@ -75,6 +86,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
result shouldBeEqualTo expectedMessage(
channel = GROUP_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -86,6 +98,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
result shouldBeEqualTo expectedMessage(
channel = GROUP_CHANNEL_ID,
shouldAlertMoreThanOnce = false,
)
}
@ -97,6 +110,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID))
result shouldBeEqualTo expectedMessage(
channel = DIRECT_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -108,6 +122,7 @@ class NotificationFactoryTest {
val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet())
result shouldBeEqualTo expectedMessage(
channel = DIRECT_CHANNEL_ID,
shouldAlertMoreThanOnce = true,
)
}
@ -119,15 +134,16 @@ class NotificationFactoryTest {
}
private fun expectedMessage(
channel: String,
shouldAlertMoreThanOnce: Boolean,
) = NotificationTypes.Room(
AndroidNotification(
channelId = "message",
channelId = SUMMARY_CHANNEL_ID,
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,
alertMoreThanOnce = false,
contentIntent = AN_OPEN_ROOM_INTENT,
messageStyle = A_NOTIFICATION_STYLE,
category = Notification.CATEGORY_MESSAGE,
@ -139,15 +155,17 @@ class NotificationFactoryTest {
summary = LATEST_EVENT.content,
messageCount = EVENTS.size,
isAlerting = shouldAlertMoreThanOnce,
summaryChannelId = channel,
)
private fun expectedSummary(shouldAlertMoreThanOnce: Boolean) = AndroidNotification(
channelId = "message",
private fun expectedSummary(channelId: String, shouldAlertMoreThanOnce: Boolean) = AndroidNotification(
channelId = channelId,
messageStyle = A_NOTIFICATION_STYLE,
alertMoreThanOnce = shouldAlertMoreThanOnce,
smallIcon = R.drawable.ic_notification_small_icon,
contentIntent = AN_OPEN_APP_INTENT,
groupId = "st",
category = Notification.CATEGORY_MESSAGE,
isGroupSummary = true,
autoCancel = true
)

View File

@ -3,8 +3,11 @@ package app.dapk.st.notifications
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeRoomStore
import fixture.*
import fixture.NotificationDiffFixtures.aNotificationDiff
import fixture.aRoomId
import fixture.aRoomMessageEvent
import fixture.aRoomOverview
import fixture.anEventId
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
@ -12,8 +15,8 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>()
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello")
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world")
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
@ -48,7 +51,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
),
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2))
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
)
}
@ -61,7 +64,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList()
result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2))
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
)
}
@ -76,6 +79,26 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
result shouldBeEqualTo emptyList()
}
@Test
fun `given new and then historical message, when reading a message, then only emits the latest`() = runTest {
fakeRoomStore.givenUnreadEvents(
flowOf(
NO_UNREADS,
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE),
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE.copy(eventId = anEventId("old"), utcTimestamp = -1))
)
)
val result = useCase.invoke().toList()
result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff(
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
),
)
}
@Test
fun `given initial unreads, when reading a duplicate unread, then emits nothing`() = runTest {
fakeRoomStore.givenUnreadEvents(

View File

@ -25,7 +25,12 @@ class RenderNotificationsUseCaseTest {
)
@Test
fun `when creating use case instance, then initiates channels`() {
fun `given events, when listening for changes then initiates channels once`() = runTest {
fakeNotificationRenderer.instance.expect { it.render(any()) }
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
renderNotificationsUseCase.listenForNotificationChanges()
fakeNotificationChannels.verifyInitiated()
}

View File

@ -14,15 +14,18 @@ object NotificationFixtures {
) = Notifications(summaryNotification, delegates)
fun aRoomNotification(
notification: AndroidNotification = anAndroidNotification(),
summary: String = "a summary line",
messageCount: Int = 1,
isAlerting: Boolean = false,
summaryChannelId: String = "a-summary-channel-id",
) = NotificationTypes.Room(
anAndroidNotification(),
notification,
aRoomId(),
summary = summary,
messageCount = messageCount,
isAlerting = isAlerting
isAlerting = isAlerting,
summaryChannelId = summaryChannelId
)
fun aDismissRoomNotification(