Skip to content

fix(notifications): rebind the listener after a disconnect - #55

Open
A-nony-mous wants to merge 1 commit into
jimrandomh:mainfrom
A-nony-mous:fix/notification-listener
Open

A-nony-mous wants to merge 1 commit into
jimrandomh:mainfrom
A-nony-mous:fix/notification-listener

Conversation

@A-nony-mous

Copy link
Copy Markdown
Contributor

Android can drop a NotificationListenerService binding without warning, and does
not restore it on its own. The app never asked for it back, so it kept answering
"no notifications" while the phone had dozens posted.

Rebind after a disconnect (backing off 1s → 32s) and log the silent path.

Reproduce while the app runs:

adb shell cmd notification disallow_listener com.faceclaw.app/com.faceclaw.app.FaceclawMediaNotificationListenerService

The Notifications app then shows the empty state; before this change it stays
that way for the life of the process.

Verified by build and install on a OnePlus 13, CPH2655.

Android can drop a NotificationListenerService binding without warning and does
not restore it on its own. onListenerDisconnected only cleared activeService, so
later reads took the null branch and answered "[]" without logging: the app
showed an empty list while the phone had dozens of notifications posted.

Ask for the binding back with requestRebind, backing off 1s -> 32s, since a
refused rebind re-enters onListenerDisconnected. Also log the null-service read.
@A-nony-mous
A-nony-mous force-pushed the fix/notification-listener branch from cc3053c to c2d4632 Compare October 23, 2026 22:51
@jimrandomh

Copy link
Copy Markdown
Owner

Opus 5.5 PR review. I'll probably have time to look more closely and iterate over the weekend:

I reviewed PR #55 and found 10 problems. The main one is that the fix probably doesn't help with the failure it's aimed at: an empty notification list after the listener binding drops. That conclusion comes from the local SDK sources. NotificationListenerService.java is from API 36, but the only local copy of ManagedServices.java is from API 28. I'm assuming newer Android behaves the same as that copy; I didn't confirm it.

All line numbers refer to FaceclawMediaNotificationListenerService.kt at the PR head.

Problems with the fix itself

  1. requestRebind does nothing here (line 721). It only undoes an earlier requestUnbind, and the app never calls requestUnbind. When the system drops the binding, nothing changes and the list still comes back as "[]". Turning the listener component off and back on with PackageManager.setComponentEnabledSetting (with DONT_KILL_APP) is the usual way to force a rebind.
  2. The repro can't tell whether the fix works. disallow_listener revokes access, which no rebind can undo. After allow_listener, Android rebinds on its own, so the before/after result says nothing about requestRebind.
  3. The backoff never grows past 1s (line 651). onListenerDisconnected is only called from onDestroy. So rebindAttempts belongs to an instance that is being destroyed, and every new instance starts back at 0. A bind-then-drop cycle would retry every second forever, which is the spin the comment says the backoff prevents. Also, a rebind the system refuses never creates an instance, so the comment's claim that it "re-enters onListenerDisconnected" is wrong.
  4. Recovery misses process death (line 682). If the process is killed (low memory, app update, crash) and Android's single rebind attempt fails, no listener instance exists in the new process. activeService stays null even though access is granted. Recovery should happen at the process level, for example on app start or when a read finds access granted but no active service.
  5. Rebinds are scheduled on every teardown (line 719). Nothing dedupes or cancels them, so they would undo a deliberate requestUnbind, the one case where requestRebind actually works. A rebind queued by the old instance also still fires after the new one has connected.
  6. Changes during the gap are lost (line 669). On reconnect, refreshActiveNotificationWakeKeys replaces the tracked keys without sending removed or posted events. A notification popup on the glasses for something dismissed during the gap stays up. Notifications posted during the gap never wake the glasses.

Logging

  1. The new Log.w (line 191) floods logcat. It fires on every paint and input of the Notifications app when there is no listener, including the normal case where access was never granted. It should be gated on isNotificationAccessEnabled() or logged once per change of state.
  2. Other silent "[]" paths are still unlogged (line 206). One is a service that has been created but never connected: activeService is set in onCreate, so the null check passes. Another is getActiveNotifications() returning null. A third is getActiveNotificationIconGrays, which returns an empty array without logging anything.

Cleanups

  1. The backoff code does nothing (line 714). Given finding 3, the constants, the counter and the shift-based doubling can be replaced by a constant delay. If real backoff is wanted, the counter has to live in the companion object.
  2. Minor:
    • The listener ComponentName is now built in three places; a shared helper would keep it in one.
    • There is a stray extra blank line near line 470.
    • The rebindAttempts field sits directly against the companion object's closing brace.

None of this has been posted to the PR. I can post these as inline comments, or work out a version of the fix that does force a rebind.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants