How SmsForwarder Handles App Notification Forwarding Using NotificationListenerService

SmsForwarder implements app notification forwarding by extending Android's NotificationListenerService in NotificationService.kt, which intercepts notifications via onNotificationPosted(), filters them against user-defined settings, packages the data into MsgInfo objects, and dispatches them to SendWorker via WorkManager for delivery.

SmsForwarder is an open-source Android application (pppscn/SmsForwarder) that forwards both SMS and app notifications to various channels like Telegram, WeChat, and email. While SMS handling uses traditional broadcast receivers, app notification forwarding relies on Android's NotificationListenerService to capture and process notifications from other applications in real-time.

Core Implementation in NotificationService.kt

The heart of the notification forwarding system resides in NotificationService.kt, where the service extends NotificationListenerService to gain access to the system notification stream.

Extending NotificationListenerService

The service runs continuously in the background while the app is active, monitoring all posted notifications across the system. When the system broadcasts a new notification, the onNotificationPosted(StatusBarNotification) callback triggers the forwarding pipeline.

The onNotificationPosted() Method

This method serves as the entry point for all notification processing. According to the source code in NotificationService.kt, it performs immediate validation before processing:

  1. Checks if the app is in pure-client mode (enablePureClientMode)
  2. Verifies the master app notification switch (enableAppNotify)
  3. Validates the screen lock state against enableNotUserPresent
  4. Extracts the package name, title, and text from the StatusBarNotification

Empty notifications (where both title and text are blank) are ignored immediately to prevent processing noise.

Configuration and Filtering Logic

Before forwarding any notification, the service validates multiple global switches stored in SettingUtils.kt. These settings provide granular control over the forwarding behavior:

  • enablePureClientMode: When true, the app functions as a pure client and ignores all forwarded notifications.
  • enableAppNotify: The master toggle that enables or disables app notification forwarding entirely.
  • enableNotUserPresent: When enabled, notifications forward only while the screen is unlocked (device is user-present).
  • cancelExtraAppNotify: A newline-separated denylist of package names whose notifications are silently dismissed without forwarding.
  • enableCancelAppNotify: If a forwarding rule matches the incoming message, the original notification is automatically cancelled from the status bar.

Message Processing and Forwarding Pipeline

Once the settings checks pass, the service extracts notification content and prepares it for forwarding.

Extracting Notification Content

For Android Lollipop (API 21) and above, the service extracts both standard and large text content to ensure full message capture:

val pkg = sbn?.packageName ?: return
val extras = sbn.notification.extras ?: return
val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: ""
val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: ""

if (title.isBlank() && text.isBlank()) return

Packaging into MsgInfo

The extracted data is encapsulated in a MsgInfo object that standardizes the message format across the application:

val msgInfo = MsgInfo(
    type = "app",
    from = sbn.packageName,
    content = text,
    receivedAt = Date(),
    title = title,
    uid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) sbn.uid else -1
)

Dispatching via WorkManager

The MsgInfo is serialized and passed to SendWorker via WorkManager, which handles the actual forwarding asynchronously according to user-defined rules:

val work = OneTimeWorkRequestBuilder<SendWorker>()
    .setInputData(workDataOf(Worker.SEND_MSG_INFO to Gson().toJson(msgInfo)))
    .build()
WorkManager.getInstance(applicationContext).enqueue(work)

Notification Cancellation and Advanced Filtering

The service provides automatic notification management based on user preferences.

Silencing Specific Apps

When cancelExtraAppNotify contains package names, the service splits the string by newlines and cancels notifications from those apps immediately:

SettingUtils.cancelExtraAppNotify
    .takeIf { it.isNotEmpty() }
    ?.split("\n")
    ?.forEach { app ->
        if (sbn.packageName == app.trim()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                cancelNotification(sbn.key)
            } else {
                cancelNotification(sbn.packageName, sbn.tag, sbn.id)
            }
        }
    }

Rule-Based Cancellation

If enableCancelAppNotify is active and a forwarding rule matches the notification content, the service calls cancelNotification() to remove the original alert from the user's status bar after successful forwarding.

Service Lifecycle and Resilience

Maintaining a persistent connection to the notification stream requires handling service disconnections gracefully.

Automatic Rebinding

In NotificationService.kt, the onListenerDisconnected() callback triggers requestRebind() (available on Android Nougat and above) to automatically re-attach the listener if the system disconnects it, ensuring continuous operation without user intervention.

Enabling and Disabling the Service

The CommonUtils.kt file provides static helpers for UI components to manage the service state:

  • isNotificationListenerServiceEnabled(context): Checks whether the user has granted notification access permission in system settings.
  • toggleNotificationListenerService(context): Programmatically opens the system "Notification access" settings page, forcing the user to enable the component if disabled.

These utilities are invoked from ForegroundService, SettingsFragment.kt, and other UI components whenever the user toggles the "App Notification" switch.

UI Integration and User Control

The SettingsFragment.kt implements the user-facing controls for notification forwarding. When a user enables the feature, the app verifies the service state:

if (SettingUtils.enableAppNotify && !CommonUtils.isNotificationListenerServiceEnabled(requireContext())) {
    // Prompt the user to enable the listener
    CommonUtils.toggleNotificationListenerService(requireContext())
}

This integration ensures that the service cannot be accidentally enabled without the proper system permissions, preventing silent failures.

Summary

  • SmsForwarder uses NotificationService.kt to extend Android's NotificationListenerService and intercept system notifications.
  • The forwarding pipeline consists of: settings validation → content extraction → MsgInfo packaging → SendWorker dispatch via WorkManager.
  • Five critical settings in SettingUtils.kt control behavior: pure-client mode, master enable switch, screen lock requirements, app-specific denylists, and automatic cancellation.
  • The service implements automatic rebinding via requestRebind() in onListenerDisconnected() to survive system disconnections.
  • CommonUtils.kt provides the bridge between UI components (SettingsFragment.kt) and the system notification access settings.

Frequently Asked Questions

What Android permissions are required for notification forwarding?

SmsForwarder requires the Notification Access permission, which grants the app permission to read all incoming notifications. This is a system-level permission that users must explicitly enable in Android Settings > Apps > Special access > Notification access. The app cannot forward notifications without this permission, as it relies on NotificationListenerService to receive the StatusBarNotification objects.

How does SmsForwarder handle service disconnections or app kills?

The service implements resilience mechanisms in NotificationService.kt. When onListenerDisconnected() is triggered, the service automatically calls requestRebind() on Android Nougat (API 24) and above to re-establish the connection to the notification stream. Additionally, the ForegroundService helps keep the process alive, though the NotificationListener itself is managed by the Android system and will restart if the app is not force-stopped.

Can users exclude specific apps from notification forwarding?

Yes, users can define a denylist using the cancelExtraAppNotify setting, which accepts a newline-separated list of package names. Notifications from these apps are intercepted and immediately cancelled via cancelNotification() without being forwarded. Additionally, the enableCancelAppNotify setting allows the app to automatically dismiss notifications from forwarded apps if a matching rule is found.

What happens if the screen is locked when a notification arrives?

Behavior depends on the enableNotUserPresent setting in SettingUtils.kt. When this setting is enabled, the service checks the device state and only forwards notifications while the screen is unlocked (user present). If the device is locked, the notification is ignored. This allows users to filter sensitive notifications that arrive while the device is unattended.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →