Repository navigation
Notification actions and dismissals fail: ID embeds postTime and lookup has no fallback #149
Description
Summary
Notification actions (Mark as read, Mute, etc.) and dismissals sent from the
Mac client fail with success=false for most notifications. The Mac side sends
the request correctly and Android receives it — performNotificationAction()
simply cannot resolve the notification ID and returns false.
In one session I saw 22 failed dismissals and 1 failed action, against a single
success.
Evidence
Two dismissal attempts for the same Google Maps notification, from one session:
[websocket] Dismissal succeeded for notification id: com.google.android.apps.maps_782481618_1787386959101
[websocket] Dismissal failed for notification id: com.google.android.apps.maps_782481618_1787386923490
Same package, same title hash (782481618), different trailing timestamp.
The newer ID resolves; the older one the client still holds does not.
An action failure from the same session:
[websocket] Notification action response id=com.whatsapp_2118324457_1787387347429 action=Mark as read success=false
Root cause
generateNotificationId() embeds postTime:
// NotificationDismissalUtil.kt:34
fun generateNotificationId(packageName: String, title: String, timestamp: Long): String {
return "${packageName}_${title.hashCode()}_$timestamp"
}Apps that update a notification in place (WhatsApp, Maps, delivery trackers)
produce a new ID on every update, while the client may still hold an earlier
one. Lookup is a single direct map hit with no fallback:
// NotificationDismissalUtil.kt:122
val sbn = activeNotifications[notificationId]
if (sbn == null) {
Log.w(TAG, "Notification with ID $notificationId not found for action $actionName")
return false
}The keyToId reverse map exists for exactly this reason — the comment at the top
of the file says it "keeps client-held IDs valid across process restarts" — but
neither performNotificationAction() nor the dismissal path ever consults it.
There is also a persistence gap: only keyToId is written to SharedPreferences.
activeNotifications holds StatusBarNotification objects, which cannot be
persisted, so after the listener service is restarted the map is empty and every
action fails until each notification is re-posted.
Suggested fix
On a miss, fall back rather than returning false immediately:
- Resolve via
keyToId(reverse lookup bysbn.key, which is stable across updates). - If still unresolved, re-scan
service.getActiveNotifications()and rebuild the
mapping — this also repairs the in-memory map after a service restart. - Optionally prefer
sbn.keyover apostTime-derived ID as the wire identifier,
since it is stable by design.
Environment
- Android app 4.0.1 (versionCode 32)
- macOS client 4.0.1
- OnePlus 15R
- Affected packages observed:
com.whatsapp,com.google.android.apps.maps,
com.application.zomato,com.android.incallui,com.google.android.odad
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsBacklog
Activity
sameerasw commentedon Aug 26, 2026
Hi! does this occur on the test version as well? (v4.0.1)
sjsingh-a commentedon Aug 26, 2026
Apologies — the version in my report was wrong. That was taken from source, not from the device; my phone is on v4.0.0, so I haven't tested v4.0.1.
I did check the 4.0.1 source though, and both parts look unchanged:
generateNotificationId()still embedspostTime, andperformNotificationAction()still returnsfalseon a direct map miss without falling back tokeyToId. So I'd expect it to reproduce there as well.