11package com.itsazni.notificationforwarder.service
22
33import android.app.Notification
4+ import android.os.Build
45import android.service.notification.NotificationListenerService
56import android.service.notification.StatusBarNotification
67import com.itsazni.notificationforwarder.data.NotificationRepository
@@ -13,6 +14,15 @@ import kotlinx.coroutines.launch
1314class AppNotificationListenerService : NotificationListenerService () {
1415 private val serviceScope = CoroutineScope (SupervisorJob () + Dispatchers .IO )
1516
17+ private data class RecentEvent (
18+ val contentHash : Int ,
19+ val postedAt : Long ,
20+ val seenAt : Long
21+ )
22+
23+ private val dedupLock = Any ()
24+ private val recentEvents = LinkedHashMap <String , RecentEvent >(MAX_RECENT_EVENTS , 0.75f , true )
25+
1626 override fun onNotificationPosted (sbn : StatusBarNotification ? ) {
1727 super .onNotificationPosted(sbn)
1828 val item = sbn ? : return
@@ -24,6 +34,11 @@ class AppNotificationListenerService : NotificationListenerService() {
2434 val extras = notification.extras
2535 val title = extras?.getCharSequence(Notification .EXTRA_TITLE )?.toString().orEmpty()
2636 val text = extras?.getCharSequence(Notification .EXTRA_TEXT )?.toString().orEmpty()
37+ val bigText = extras?.getCharSequence(Notification .EXTRA_BIG_TEXT )?.toString().orEmpty()
38+
39+ if (shouldSkip(item, notification, title, text, bigText)) {
40+ return
41+ }
2742
2843 serviceScope.launch {
2944 val repository = NotificationRepository (applicationContext)
@@ -46,4 +61,71 @@ class AppNotificationListenerService : NotificationListenerService() {
4661 pm.getApplicationLabel(applicationInfo).toString()
4762 }.getOrDefault(pkg)
4863 }
64+
65+ private fun shouldSkip (
66+ sbn : StatusBarNotification ,
67+ notification : Notification ,
68+ title : String ,
69+ text : String ,
70+ bigText : String
71+ ): Boolean {
72+ val isGroupSummary = (notification.flags and Notification .FLAG_GROUP_SUMMARY ) != 0
73+ if (isGroupSummary) {
74+ return true
75+ }
76+
77+ val stableKey = buildStableKey(sbn)
78+ val contentHash = listOf (title, text, bigText).joinToString(" \u001f " ).hashCode()
79+ val now = System .currentTimeMillis()
80+
81+ synchronized(dedupLock) {
82+ val previous = recentEvents[stableKey]
83+ if (previous != null ) {
84+ val sameContent = previous.contentHash == contentHash
85+ val samePostTime = previous.postedAt == sbn.postTime
86+ val burstUpdate = now - previous.seenAt <= DUPLICATE_WINDOW_MS
87+ if (sameContent && (samePostTime || burstUpdate)) {
88+ return true
89+ }
90+ }
91+
92+ recentEvents[stableKey] = RecentEvent (
93+ contentHash = contentHash,
94+ postedAt = sbn.postTime,
95+ seenAt = now
96+ )
97+ trimRecentEvents()
98+ }
99+
100+ return false
101+ }
102+
103+ private fun buildStableKey (sbn : StatusBarNotification ): String {
104+ val fallback = buildString {
105+ append(sbn.packageName)
106+ append(' |' )
107+ append(sbn.id)
108+ append(' |' )
109+ append(sbn.tag ? : " " )
110+ append(' |' )
111+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .JELLY_BEAN_MR1 ) {
112+ append(sbn.user.hashCode())
113+ } else {
114+ append(" legacy-user" )
115+ }
116+ }
117+ return sbn.key.ifBlank { fallback }
118+ }
119+
120+ private fun trimRecentEvents () {
121+ while (recentEvents.size > MAX_RECENT_EVENTS ) {
122+ val firstKey = recentEvents.entries.firstOrNull()?.key ? : return
123+ recentEvents.remove(firstKey)
124+ }
125+ }
126+
127+ companion object {
128+ private const val DUPLICATE_WINDOW_MS = 500L
129+ private const val MAX_RECENT_EVENTS = 512
130+ }
49131}
0 commit comments