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 { companion object {
private const val NOTIFICATION_ID = 9999 internal const val NOTIFICATION_ID = 9999
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, MessagingService::class.java) val intent = Intent(context, MessagingService::class.java)

View file

@ -108,71 +108,38 @@ internal class NotificationHelper @Inject constructor(
): Boolean { ): Boolean {
if (shouldSuppressNotification(senderId)) return false 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 senderNotificationId = senderId.hashCode()
val notificationManager = NotificationManagerCompat.from(context) val (contentText, pendingIntent) = synchronized(activeSenderNotificationIds) {
try {
val (shouldShowSummary, summaryText) = synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.add(senderNotificationId) activeSenderNotificationIds.add(senderNotificationId)
activeSenderNamesById[senderNotificationId] = senderUsername activeSenderNamesById[senderNotificationId] = senderUsername
val names = activeSenderNamesById.values.toList()
if (activeSenderNotificationIds.size == 1) {
Pair( Pair(
activeSenderNotificationIds.size > 1, "Neue Nachricht von $senderUsername",
buildSummaryText(activeSenderNamesById.values.toList()) 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) .setSmallIcon(R.drawable.ic_notification_message)
.setContentTitle("Neue Nachrichten") .setContentTitle("Bollwerk")
.setContentText(summaryText) .setContentText(contentText)
.setGroup(GROUP_KEY) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setGroupSummary(true) .setAutoCancel(false)
.setAutoCancel(true) .setOngoing(true)
.setContentIntent(messagesPendingIntent) .setContentIntent(pendingIntent)
.build() .build()
notificationManager.notify(senderNotificationId, notification) val notificationManager = NotificationManagerCompat.from(context)
if (shouldShowSummary) { try {
notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification) notificationManager.notify(MessagingService.NOTIFICATION_ID, messageNotification)
} else {
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
}
} catch (_: SecurityException) { } catch (_: SecurityException) {
// Permission not granted ignore silently
return false return false
} }
return true return true
@ -188,66 +155,53 @@ internal class NotificationHelper @Inject constructor(
} }
/// Entfernt Benachrichtigungen für einen bestimmten Absender (wenn der Chat geöffnet wird). /// 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) { fun cancelNotificationForSender(senderId: String) {
val notificationManager = NotificationManagerCompat.from(context)
val senderNotificationId = senderId.hashCode() val senderNotificationId = senderId.hashCode()
notificationManager.cancel(senderNotificationId) val remainingText: String? = synchronized(activeSenderNotificationIds) {
val hasMoreSenders = synchronized(activeSenderNotificationIds) {
activeSenderNotificationIds.remove(senderNotificationId) activeSenderNotificationIds.remove(senderNotificationId)
activeSenderNamesById.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. /// Aktualisiert den Launcher-Badge-Zähler für ungelesene Nachrichten.
fun updateBadgeCount(count: Int) { fun updateBadgeCount(count: Int) {
val notificationManager = NotificationManagerCompat.from(context) // Badge-Logik ist mit der vereinheitlichten Notification redundant.
if (count <= 0) { // Kein separater Badge-Notification nötig, da die FG-Notification immer sichtbar ist.
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) {}
} }
fun cancelAllMessageNotifications() { fun cancelAllMessageNotifications() {
val notificationManager = NotificationManagerCompat.from(context) synchronized(activeSenderNotificationIds) {
val ids = synchronized(activeSenderNotificationIds) {
val copy = activeSenderNotificationIds.toList()
activeSenderNotificationIds.clear() activeSenderNotificationIds.clear()
activeSenderNamesById.clear() activeSenderNamesById.clear()
copy
} }
ids.forEach { notificationManager.cancel(it) } val notificationManager = NotificationManagerCompat.from(context)
notificationManager.cancel(SUMMARY_NOTIFICATION_ID) try {
notificationManager.notify(MessagingService.NOTIFICATION_ID, buildIdleNotification())
} catch (_: SecurityException) {}
} }
private fun buildSummaryText(senderNames: List<String>): String { 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 { companion object {
const val CHANNEL_ID = "bollwerk_messages" const val CHANNEL_ID = "bollwerk_messages"
private const val CHANNEL_NAME = "Chat-Nachrichten" private const val CHANNEL_NAME = "Chat-Nachrichten"
const val SERVICE_CHANNEL_ID = "bollwerk_service" const val SERVICE_CHANNEL_ID = "bollwerk_service"
private const val SERVICE_CHANNEL_NAME = "Nachrichtendienst" private const val SERVICE_CHANNEL_NAME = "Nachrichtendienst"
private const val GROUP_KEY = "de.bollwerk.app.MESSAGES" private const val MESSAGES_REQUEST_CODE = 1001
private const val SUMMARY_NOTIFICATION_ID = 0
const val EXTRA_RECIPIENT_ID = "notification_recipient_id" const val EXTRA_RECIPIENT_ID = "notification_recipient_id"
const val EXTRA_RECIPIENT_USERNAME = "notification_recipient_username" const val EXTRA_RECIPIENT_USERNAME = "notification_recipient_username"