Skip to content

feat: Android → Mac notification sync over BLE#72

Open
vegaglitch wants to merge 4 commits into
geekflyer:mainfrom
vegaglitch:main
Open

feat: Android → Mac notification sync over BLE#72
vegaglitch wants to merge 4 commits into
geekflyer:mainfrom
vegaglitch:main

Conversation

@vegaglitch
Copy link
Copy Markdown

Summary

This adds real-time notification mirroring from Android to Mac over the existing BLE L2CAP connection — no new pairing, no new infrastructure.

  • Android: A NotificationListenerService captures posted notifications, resolves the human-readable app name, renders the app icon as a 64×64 PNG, and sends everything to Mac as an encrypted BLE notification message. A settings toggle in the main UI lets the user enable/disable the feature.
  • Mac: Decrypts the incoming notification message, extracts the icon bytes, and posts a native macOS UNUserNotification with the title, body, app-name subtitle, and icon attachment.
  • Protocol fix: didReceiveNotification was only declared in a SessionDelegate extension (static dispatch), so it silently hit the empty default. Moving it into the protocol declaration restores dynamic dispatch.

What it looks like

Android notifications appear on Mac with:

  • Notification title as the banner title
  • App name as the subtitle
  • App icon on the right side of the banner

Test plan

  • Pair Android device with Mac via existing QR flow
  • Enable "Notification Sync" toggle in the Android app settings
  • Grant Notification Listener permission when prompted
  • Trigger a notification on Android (e.g. Gmail, WhatsApp) — it should appear on Mac within ~1s
  • Disable the toggle — notifications should stop mirroring
  • Verify clipboard sync still works normally alongside notification sync

🤖 Generated with Claude Code

vegaglitch and others added 4 commits May 12, 2026 11:51
- Add NotificationListenerService to capture posted notifications
- Add NotificationSettingsStore to persist the notification sync toggle
- Extract app display name (QUERY_ALL_PACKAGES) and 64×64 PNG icon
  (base64-encoded) from each notification and include in the BLE payload
- Add notification message type to MessageCodec and Session
- Wire notification sync toggle into the settings UI (ClipRelayScreen,
  MainActivity, MainViewModel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add notification message type to MessageCodec and Session
- Fix protocol dispatch: add didReceiveNotification to SessionDelegate
  protocol declaration so conforming types are called (was extension-only,
  causing silent static-dispatch to the empty default)
- Thread iconData (base64 PNG from Android) through Session →
  ConnectionController → AppDelegate → ReceiveNotificationManager
- Attach icon as UNNotificationAttachment so it appears in the
  macOS notification banner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android:
- NotificationRelayService: add icon extraction (renderIconBase64) so
  iconPng is actually included in the BLE payload; move TAG and
  settingsStore to companion/onCreate; remove empty onNotificationRemoved;
  reduce debug logging
- MainActivity/MainViewModel: replace remember(Unit) for
  notificationListenerGranted with a ViewModel StateFlow refreshed in
  onResume, so the UI updates correctly after the user grants permission
  and returns to the app; import NotificationRelayService/
  NotificationSettingsStore instead of using fully-qualified names inline

macOS:
- AppDelegate: restore SMAppService launch-at-login support that was
  accidentally removed when porting from NotiSyncMac
- ReceiveNotificationManager: delete temp PNG file after UNNotification
  Attachment is created to avoid accumulating files in the temp directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…msung

Samsung OneUI blocks NotificationListenerService callbacks for sideloaded
APKs, so onNotificationPosted never fires. Instead, hook into the existing
ClipboardAccessibilityService by adding TYPE_NOTIFICATION_STATE_CHANGED
to its event mask and handling it independently of the auto-copy setting.

Also add QUERY_ALL_PACKAGES permission so packageManager.getApplicationIcon()
resolves icons for all installed apps on Android 11+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaglitch
Copy link
Copy Markdown
Author

Note on dual notification capture paths

The PR currently has two mechanisms for capturing Android notifications:

  1. NotificationListenerService (NotificationRelayService.kt) — the standard Android approach, gives clean access to all notification metadata.
  2. AccessibilityService + TYPE_NOTIFICATION_STATE_CHANGED (ClipboardAccessibilityService.kt) — added as a workaround because Samsung OneUI blocks NotificationListenerService callbacks for sideloaded APKs (the onNotificationPosted method is simply never called on Samsung devices when the app isn't installed via Play Store).

On non-Samsung devices, both paths will fire for the same notification. The Mac side won't show duplicates because UNUserNotificationRequest uses a hash of (appName, title) as the identifier, so a second delivery just replaces the first. But it does mean two BLE messages are sent per notification, which is wasteful.

A cleaner long-term fix would be one of:

  • Remove NotificationRelayService entirely and rely solely on the accessibility approach (works universally, slightly less metadata fidelity)
  • Keep both but add a short-window dedup on the Android side (e.g. a timestamp+title cache) so the accessibility path skips notifications already sent by the listener service
  • On Play Store builds (where NotificationListenerService works on Samsung too), the accessibility path could be disabled

Happy to implement whichever approach you prefer before merge.

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.

1 participant