feat: notification dots follow AOSP badge count semantics

NotificationBadgeService now sums Notification.number (or 1 when unset)
per package across counted notifications, matching Launcher3 badge
behavior. A single K-9 notification with number=14 (unread count) now
yields a dot, not just one notification = one dot.
This commit is contained in:
2026-08-19 12:14:52 +02:00
parent c972125fef
commit 84fdb81061
3 changed files with 24 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ package com.github.droidworksstudio.launcher.service
import android.app.Notification import android.app.Notification
import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -16,36 +17,43 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
class NotificationBadgeService : NotificationListenerService() { class NotificationBadgeService : NotificationListenerService() {
/** Notification key -> "userId/packageName". Keyed to dedupe updates. */ /** Notification key -> which app it belongs to and its badge number. */
private val keyToPackage = HashMap<String, String>() private val keyToEntry = HashMap<String, BadgeEntry>()
private data class BadgeEntry(val packageKey: String, val number: Int)
override fun onListenerConnected() { override fun onListenerConnected() {
super.onListenerConnected() super.onListenerConnected()
synchronized(keyToPackage) { synchronized(keyToEntry) {
keyToPackage.clear() keyToEntry.clear()
activeNotifications.forEach { sbn -> activeNotifications.forEach { sbn ->
if (isCounted(sbn)) keyToPackage[sbn.key] = userIdKey(sbn) if (isCounted(sbn)) keyToEntry[sbn.key] = entryFor(sbn)
} }
publish() publish()
} }
Log.d("BadgeService", "connected, active=" + activeNotifications.size +
" counts=" + notificationCounts.value)
} }
override fun onNotificationPosted(sbn: StatusBarNotification) { override fun onNotificationPosted(sbn: StatusBarNotification) {
if (!isCounted(sbn)) return if (!isCounted(sbn)) return
synchronized(keyToPackage) { synchronized(keyToEntry) {
// A posted notification with an existing key is just an update. // A posted notification with an existing key is just an update.
if (keyToPackage.containsKey(sbn.key)) return if (keyToEntry.containsKey(sbn.key)) return
keyToPackage[sbn.key] = userIdKey(sbn) keyToEntry[sbn.key] = entryFor(sbn)
publish() publish()
} }
} }
override fun onNotificationRemoved(sbn: StatusBarNotification) { override fun onNotificationRemoved(sbn: StatusBarNotification) {
synchronized(keyToPackage) { synchronized(keyToEntry) {
if (keyToPackage.remove(sbn.key) != null) publish() if (keyToEntry.remove(sbn.key) != null) publish()
} }
} }
private fun entryFor(sbn: StatusBarNotification) =
BadgeEntry(userIdKey(sbn), sbn.notification.number)
/** Only swipe-away, non-ongoing notifications get a dot. */ /** Only swipe-away, non-ongoing notifications get a dot. */
private fun isCounted(sbn: StatusBarNotification): Boolean { private fun isCounted(sbn: StatusBarNotification): Boolean {
val notification = sbn.notification val notification = sbn.notification
@@ -58,9 +66,13 @@ class NotificationBadgeService : NotificationListenerService() {
"${sbn.userId}/" + sbn.packageName "${sbn.userId}/" + sbn.packageName
private fun publish() { private fun publish() {
// AOSP launcher badge semantics: for each counted notification add
// its badge number (Notification.number), or 1 when unset, so e.g.
// K-9's unread-count badge shows a dot.
val counts = HashMap<String, Int>() val counts = HashMap<String, Int>()
keyToPackage.values.forEach { key -> keyToEntry.values.forEach { entry ->
counts[key] = (counts[key] ?: 0) + 1 val increment = entry.number.coerceAtLeast(1)
counts[entry.packageKey] = (counts[entry.packageKey] ?: 0) + increment
} }
notificationCounts.value = counts notificationCounts.value = counts
} }

Binary file not shown.