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 by sbn.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.key over a postTime-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
Summary
Notification actions (
Mark as read,Mute, etc.) and dismissals sent from theMac client fail with
success=falsefor most notifications. The Mac side sendsthe 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:
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:
Root cause
generateNotificationId()embedspostTime: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:
The
keyToIdreverse map exists for exactly this reason — the comment at the topof 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
keyToIdis written toSharedPreferences.activeNotificationsholdsStatusBarNotificationobjects, which cannot bepersisted, 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
falseimmediately:keyToId(reverse lookup bysbn.key, which is stable across updates).service.getActiveNotifications()and rebuild themapping — this also repairs the in-memory map after a service restart.
sbn.keyover apostTime-derived ID as the wire identifier,since it is stable by design.
Environment
com.whatsapp,com.google.android.apps.maps,com.application.zomato,com.android.incallui,com.google.android.odad