fix(notifications): merge FG-service and message notifications into one

Use a single notification ID (9999) for both idle foreground-service state
and incoming-message alerts. Shows sender name for 1 message, summary for
multiple senders. Cancelling resets to idle instead of removing.

Closes #111
This commit is contained in:
Jens Reinemann 2026-05-18 21:23:58 +02:00
parent b7ef6af0a4
commit 9eefa79c64
2 changed files with 105 additions and 106 deletions

View file

@ -73,7 +73,7 @@ internal class MessagingService : Service() {
}
companion object {
private const val NOTIFICATION_ID = 9999
internal const val NOTIFICATION_ID = 9999
fun start(context: Context) {
val intent = Intent(context, MessagingService::class.java)

View file

@ -108,71 +108,38 @@ internal class NotificationHelper @Inject constructor(
): Boolean {
if (shouldSuppressNotification(senderId)) return false
val chatIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_RECIPIENT_ID, senderId)
putExtra(EXTRA_RECIPIENT_USERNAME, senderUsername)
}
val chatPendingIntent = PendingIntent.getActivity(
context,
senderId.hashCode(),
chatIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val messagesIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_OPEN_MESSAGES, true)
}
val messagesPendingIntent = PendingIntent.getActivity(
context,
SUMMARY_NOTIFICATION_ID,
messagesIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_message)
.setContentTitle("Neue Nachricht")
.setContentText("$senderUsername hat etwas geschrieben")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(chatPendingIntent)
.setGroup(GROUP_KEY)
.build()
val senderNotificationId = senderId.hashCode()
val notificationManager = NotificationManagerCompat.from(context)
try {
val (shouldShowSummary, summaryText) = synchronized(activeSenderNotificationIds) {
val (contentText, pendingIntent) = synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.add(senderNotificationId)
activeSenderNamesById[senderNotificationId] = senderUsername
val names = activeSenderNamesById.values.toList()
if (activeSenderNotificationIds.size == 1) {
Pair(
activeSenderNotificationIds.size > 1,
buildSummaryText(activeSenderNamesById.values.toList())
"Neue Nachricht von $senderUsername",
buildChatPendingIntent(senderId, senderUsername)
)
} else {
Pair(
buildSummaryText(names),
buildMessagesPendingIntent()
)
}
}
val summaryNotification = NotificationCompat.Builder(context, CHANNEL_ID)
val messageNotification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_message)
.setContentTitle("Neue Nachrichten")
.setContentText(summaryText)
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setAutoCancel(true)
.setContentIntent(messagesPendingIntent)
.setContentTitle("Bollwerk")
.setContentText(contentText)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(false)
.setOngoing(true)
.setContentIntent(pendingIntent)
.build()
notificationManager.notify(senderNotificationId, notification)
if (shouldShowSummary) {
notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification)
} else {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
}
val notificationManager = NotificationManagerCompat.from(context)
try {
notificationManager.notify(MessagingService.NOTIFICATION_ID, messageNotification)
} catch (_: SecurityException) {
// Permission not granted ignore silently
return false
}
return true
@ -188,66 +155,53 @@ internal class NotificationHelper @Inject constructor(
}
/// Entfernt Benachrichtigungen für einen bestimmten Absender (wenn der Chat geöffnet wird).
/// Wenn keine aktiven Absender mehr vorhanden sind, wird auf die Idle-Notification zurückgesetzt.
fun cancelNotificationForSender(senderId: String) {
val notificationManager = NotificationManagerCompat.from(context)
val senderNotificationId = senderId.hashCode()
notificationManager.cancel(senderNotificationId)
val hasMoreSenders = synchronized(activeSenderNotificationIds) {
val remainingText: String? = synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.remove(senderNotificationId)
activeSenderNamesById.remove(senderNotificationId)
activeSenderNotificationIds.isNotEmpty()
if (activeSenderNotificationIds.isEmpty()) {
null
} else {
buildSummaryText(activeSenderNamesById.values.toList())
}
if (!hasMoreSenders) {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
}
val notificationManager = NotificationManagerCompat.from(context)
try {
if (remainingText == null) {
notificationManager.notify(MessagingService.NOTIFICATION_ID, buildIdleNotification())
} else {
val updatedNotification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_message)
.setContentTitle("Bollwerk")
.setContentText(remainingText)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(false)
.setOngoing(true)
.setContentIntent(buildMessagesPendingIntent())
.build()
notificationManager.notify(MessagingService.NOTIFICATION_ID, updatedNotification)
}
} catch (_: SecurityException) {}
}
/// Aktualisiert den Launcher-Badge-Zähler für ungelesene Nachrichten.
fun updateBadgeCount(count: Int) {
val notificationManager = NotificationManagerCompat.from(context)
if (count <= 0) {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
return
}
// If per-sender notifications are active, the summary notification is already informative.
val hasSenderNotifications = synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.isNotEmpty()
}
if (hasSenderNotifications) return
val messagesIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_OPEN_MESSAGES, true)
}
val pendingIntent = PendingIntent.getActivity(
context, SUMMARY_NOTIFICATION_ID, messagesIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val badgeNotification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_message)
.setContentTitle("Ungelesene Nachrichten")
.setContentText("$count ungelesene Nachrichten")
.setNumber(count)
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.build()
try {
notificationManager.notify(SUMMARY_NOTIFICATION_ID, badgeNotification)
} catch (_: SecurityException) {}
// Badge-Logik ist mit der vereinheitlichten Notification redundant.
// Kein separater Badge-Notification nötig, da die FG-Notification immer sichtbar ist.
}
fun cancelAllMessageNotifications() {
val notificationManager = NotificationManagerCompat.from(context)
val ids = synchronized(activeSenderNotificationIds) {
val copy = activeSenderNotificationIds.toList()
synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.clear()
activeSenderNamesById.clear()
copy
}
ids.forEach { notificationManager.cancel(it) }
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
val notificationManager = NotificationManagerCompat.from(context)
try {
notificationManager.notify(MessagingService.NOTIFICATION_ID, buildIdleNotification())
} catch (_: SecurityException) {}
}
private fun buildSummaryText(senderNames: List<String>): String {
@ -263,13 +217,58 @@ internal class NotificationHelper @Inject constructor(
}
}
private fun buildIdleNotification(): android.app.Notification {
val openAppIntent = PendingIntent.getActivity(
context, 0,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
},
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(context, SERVICE_CHANNEL_ID)
.setContentTitle("Bollwerk")
.setContentText("Warten auf Nachrichten…")
.setSmallIcon(R.drawable.ic_notification_message)
.setOngoing(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setContentIntent(openAppIntent)
.build()
}
private fun buildChatPendingIntent(senderId: String, senderUsername: String): PendingIntent {
val chatIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_RECIPIENT_ID, senderId)
putExtra(EXTRA_RECIPIENT_USERNAME, senderUsername)
}
return PendingIntent.getActivity(
context,
senderId.hashCode(),
chatIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
private fun buildMessagesPendingIntent(): PendingIntent {
val messagesIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_OPEN_MESSAGES, true)
}
return PendingIntent.getActivity(
context,
MESSAGES_REQUEST_CODE,
messagesIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
companion object {
const val CHANNEL_ID = "bollwerk_messages"
private const val CHANNEL_NAME = "Chat-Nachrichten"
const val SERVICE_CHANNEL_ID = "bollwerk_service"
private const val SERVICE_CHANNEL_NAME = "Nachrichtendienst"
private const val GROUP_KEY = "de.bollwerk.app.MESSAGES"
private const val SUMMARY_NOTIFICATION_ID = 0
private const val MESSAGES_REQUEST_CODE = 1001
const val EXTRA_RECIPIENT_ID = "notification_recipient_id"
const val EXTRA_RECIPIENT_USERNAME = "notification_recipient_username"