Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class NotificationActionReceiver : BroadcastReceiver() {
const val ACTION_MARK_READ = "com.flipcash.app.notifications.ACTION_MARK_READ"
const val KEY_CHAT_ID_HEX = "chat_id_hex"
const val KEY_NOTIFICATION_ID = "notification_id"
const val KEY_GROUP_KEY = "group_key"
const val KEY_TEXT_REPLY = "key_text_reply"
}

Expand Down Expand Up @@ -70,8 +69,6 @@ class NotificationActionReceiver : BroadcastReceiver() {
?.toString()
?: return

val groupKey = intent.getStringExtra(KEY_GROUP_KEY)

chatCoordinator.sendMessage(chatId, replyText)
.onSuccess {
val notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, chatId.hashCode())
Expand All @@ -96,12 +93,9 @@ class NotificationActionReceiver : BroadcastReceiver() {
.setColor(context.getColor(R.color.notification_color))
.setStyle(existingStyle)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.addAction(buildReplyAction(context, chatId, notificationId, groupKey))
.addAction(buildMarkAsReadAction(context, chatId, groupKey))
.addAction(buildReplyAction(context, chatId, notificationId))
.addAction(buildMarkAsReadAction(context, chatId))
.setAutoCancel(true)
.apply {
if (groupKey != null) setGroup(groupKey)
}
.build()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
Expand Down Expand Up @@ -129,7 +123,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
}

@OptIn(ExperimentalStdlibApi::class)
private fun buildReplyAction(context: Context, chatId: ChatId, notificationId: Int, groupKey: String?): NotificationCompat.Action {
private fun buildReplyAction(context: Context, chatId: ChatId, notificationId: Int): NotificationCompat.Action {
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(context.getString(R.string.notification_action_reply))
.build()
Expand All @@ -138,7 +132,6 @@ class NotificationActionReceiver : BroadcastReceiver() {
action = ACTION_REPLY
putExtra(KEY_CHAT_ID_HEX, chatId.bytes.toHexString())
putExtra(KEY_NOTIFICATION_ID, notificationId)
if (groupKey != null) putExtra(KEY_GROUP_KEY, groupKey)
}

val pendingIntent = PendingIntent.getBroadcast(
Expand All @@ -156,11 +149,10 @@ class NotificationActionReceiver : BroadcastReceiver() {
}

@OptIn(ExperimentalStdlibApi::class)
private fun buildMarkAsReadAction(context: Context, chatId: ChatId, groupKey: String?): NotificationCompat.Action {
private fun buildMarkAsReadAction(context: Context, chatId: ChatId): NotificationCompat.Action {
val intent = Intent(context, NotificationActionReceiver::class.java).apply {
action = ACTION_MARK_READ
putExtra(KEY_CHAT_ID_HEX, chatId.bytes.toHexString())
if (groupKey != null) putExtra(KEY_GROUP_KEY, groupKey)
}

val pendingIntent = PendingIntent.getBroadcast(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.flipcash.app.notifications

/**
* The group a notification is posted under, or null to post it ungrouped with no summary.
*
* A chat is never grouped. It posts one notification, under its chat id, and folds every new
* message into it, so a group of its own would only ever hold that one child. Android 16 force
* groups exactly that shape: it cancels the app's summary and moves the child into a system
* group, and every new message moves the child back out for a few seconds before it is moved in
* again. The summary has no content, so for those seconds it shows as an empty notification.
*
* Other pushes keep the payload's key, because they post a new notification each time and several
* can share one group.
*
* @param payloadGroupKey `NotificationPayload.groupKey`, where blank means none
* @param isChat whether the notification is rendered as a chat conversation
*/
fun planNotificationGroup(payloadGroupKey: String?, isChat: Boolean): String? {
if (isChat) return null
return payloadGroupKey?.takeIf { it.isNotEmpty() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ class NotificationService : FirebaseMessagingService(),
)
}

val isChat = chatId != null && styling != null
val groupKey = payload?.groupKey?.takeIf { it.isNotEmpty() }
val group = planNotificationGroup(payloadGroupKey = groupKey, isChat = isChat)

val builder = NotificationCompat.Builder(this, channel.id)
.setPriority(NotificationCompat.PRIORITY_HIGH)
Expand All @@ -280,7 +282,7 @@ class NotificationService : FirebaseMessagingService(),
.setAutoCancel(true)
.setContentIntent(buildContentIntent(styling?.chatType, payload?.navigation))
.apply {
if (groupKey != null) setGroup(groupKey)
if (group != null) setGroup(group)
}

val notificationId = if (chatId != null && styling != null) {
Expand All @@ -292,16 +294,16 @@ class NotificationService : FirebaseMessagingService(),

notificationManager.notify(notificationId, builder.build())

if (groupKey != null) {
if (group != null) {
val summary = NotificationCompat.Builder(this, channel.id)
.setSmallIcon(R.drawable.flipcash_logo)
.setColor(getColor(R.color.notification_color))
.setGroup(groupKey)
.setGroup(group)
.setGroupSummary(true)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setAutoCancel(true)
.build()
notificationManager.notify(groupKey.hashCode(), summary)
notificationManager.notify(group.hashCode(), summary)
}
}

Expand Down Expand Up @@ -413,8 +415,8 @@ class NotificationService : FirebaseMessagingService(),

setStyle(style)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.addAction(buildReplyAction(chatId, notificationId, groupKey))
.addAction(buildMarkAsReadAction(chatId, groupKey))
.addAction(buildReplyAction(chatId, notificationId))
.addAction(buildMarkAsReadAction(chatId))

return notificationId
}
Expand Down Expand Up @@ -529,7 +531,7 @@ class NotificationService : FirebaseMessagingService(),
}

@OptIn(ExperimentalStdlibApi::class)
private fun buildReplyAction(chatId: ChatId, notificationId: Int, groupKey: String?): NotificationCompat.Action {
private fun buildReplyAction(chatId: ChatId, notificationId: Int): NotificationCompat.Action {
val remoteInput = RemoteInput.Builder(NotificationActionReceiver.KEY_TEXT_REPLY)
.setLabel(getString(R.string.notification_action_reply))
.build()
Expand All @@ -538,7 +540,6 @@ class NotificationService : FirebaseMessagingService(),
action = NotificationActionReceiver.ACTION_REPLY
putExtra(NotificationActionReceiver.KEY_CHAT_ID_HEX, chatId.bytes.toHexString())
putExtra(NotificationActionReceiver.KEY_NOTIFICATION_ID, notificationId)
if (groupKey != null) putExtra(NotificationActionReceiver.KEY_GROUP_KEY, groupKey)
}

val pendingIntent = PendingIntent.getBroadcast(
Expand All @@ -556,11 +557,10 @@ class NotificationService : FirebaseMessagingService(),
}

@OptIn(ExperimentalStdlibApi::class)
private fun buildMarkAsReadAction(chatId: ChatId, groupKey: String?): NotificationCompat.Action {
private fun buildMarkAsReadAction(chatId: ChatId): NotificationCompat.Action {
val intent = Intent(this, NotificationActionReceiver::class.java).apply {
action = NotificationActionReceiver.ACTION_MARK_READ
putExtra(NotificationActionReceiver.KEY_CHAT_ID_HEX, chatId.bytes.toHexString())
if (groupKey != null) putExtra(NotificationActionReceiver.KEY_GROUP_KEY, groupKey)
}

val pendingIntent = PendingIntent.getBroadcast(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.flipcash.app.notifications

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class NotificationGroupPlannerTest {

@Test
fun `chat notification is not grouped`() {
// One notification per chat, so its group would only ever hold that one. Android 16 cancels
// the summary of a group like that and regroups the child, and the summary shows empty
// in between.
assertNull(planNotificationGroup(payloadGroupKey = "chat-a", isChat = true))
}

@Test
fun `non-chat notification keeps the payload's group`() {
// Pushes such as CONTACT_JOIN post a fresh notification each time under one key.
assertEquals("CONTACT_JOIN", planNotificationGroup(payloadGroupKey = "CONTACT_JOIN", isChat = false))
}

@Test
fun `blank group key is no group`() {
assertNull(planNotificationGroup(payloadGroupKey = "", isChat = false))
assertNull(planNotificationGroup(payloadGroupKey = null, isChat = false))
}
}
Loading