Smart Notification Listener
A robust Flutter plugin for Android that allows your app to listen to system notifications in real-time and send replies directly to supported apps like WhatsApp, Telegram, and more.
This plugin uses a custom Native Kotlin implementation to handle Android's NotificationListenerService, offering better reliability and connection recovery than older packages.
๐ง Features
- โ Real-time Listening: Capture title, text, package name, and large icons.
- ๐ค Smart Replies: Reply directly to notifications that support input (e.g., messaging apps).
- ๐ Auto-Reconnect: Includes tools to force-restart the service if Android kills it.
- ๐ฅ Health Monitoring: Detect connection status and recover automatically.
- ๐ Privacy Focused: Requires only Notification Access, no intrusive permissions.
๐ฆ Installation
In your pubspec.yaml:
dependencies:
smart_notification_listener:
path: ../smart_notification_listener # Use path for local testing or Git URL
๐ฑ Android Setup
Notification listener service (no manual manifest entry)
The plugin ships a NotificationListenerService entry in its own AndroidManifest.xml. Flutter merges that into your app, so you should not declare the same <service> again in android/app/src/main/AndroidManifest.xml (duplicate entries cause manifest merger errors).
Do not add android.permission.BIND_NOTIFICATION_LISTENER_SERVICE as a <uses-permission>; that permission is system-only. Binding is enforced with android:permission on the merged <service> declaration.
At runtime, the user must enable Notification access for your app (openNotificationSettings()). Optional extras such as RECEIVE_BOOT_COMPLETED or WAKE_LOCK are only needed if you add a BroadcastReceiver or other components; they are not required by this plugin alone.
Enable Kotlin (only if not already enabled)
Ensure Kotlin is set up in your app. If you're using a recent version of Flutter and the Android Gradle Plugin, Kotlin is likely already enabled.
A. If using the older Gradle setup (build.gradle):
buildscript {
ext.kotlin_version = '1.9.0' // Or newer
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
B. If using the modern Gradle setup (plugins block): Make sure your android/build.gradle or settings.gradle includes:
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
}
And in your app/build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
๐งช Example Usage
import 'dart:async';
import 'dart:developer'; // For log()
import 'package:flutter/material.dart';
import 'package:smart_notification_listener/smart_notification_listener.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
final SmartNotificationListener _plugin = SmartNotificationListener();
StreamSubscription<SmartNotification>? _subscription;
String _status = "Idle";
String _lastNotification = "None";
bool _isHealthy = false;
Timer? _healthMonitorTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Auto-start listening on launch
startListening();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_subscription?.cancel();
_healthMonitorTimer?.cancel();
super.dispose();
}
// Handle App Lifecycle: Reconnect when app comes to foreground
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
log("๐ App Resumed - Refreshing connection...");
startListening();
}
}
void startListening() {
// 1. Cancel old subscription
_subscription?.cancel();
// 2. Listen to stream
_subscription = _plugin.notifications.listen((event) {
// Handle Control Signals (Native side sends 'connected' on service start)
if (event.packageName == 'connected') {
setState(() {
_status = "Connected โ
";
_isHealthy = true;
});
return;
}
// Handle Actual Notifications
setState(() {
_lastNotification = "${event.title}: ${event.text}";
});
});
// 3. Start Health Monitor
_startHealthMonitor();
}
/// Checks if the service is running and forces a reconnect if needed
void _startHealthMonitor() {
_healthMonitorTimer?.cancel();
int checks = 0;
_healthMonitorTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
checks++;
// Stop checking after 30s or if connection is established
if (checks > 30 || _isHealthy) {
timer.cancel();
return;
}
bool hasPermission = await _plugin.hasPermission();
// If we have permission but no connection signal, kick the service
if (hasPermission && !_isHealthy) {
log("โก Health Monitor: Kicking Service...");
await _plugin.forceReconnect();
}
});
}
Future<void> requestPermissions() async {
// Start monitor BEFORE going to settings so we catch the connection on return
_startHealthMonitor();
await _plugin.openNotificationSettings();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Smart Notification Listener')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Status: $_status', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
const Text('Last Notification:', style: TextStyle(fontWeight: FontWeight.bold)),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(_lastNotification, textAlign: TextAlign.center),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: requestPermissions,
child: const Text('Grant Permissions / Fix Connection'),
),
],
),
),
),
);
}
}
๐ Available Methods
| Method | Description |
|---|---|
startNotificationService() |
Asks the system to bind the listener (API 24+ requestRebind; below 24 uses a component toggle). Requires notification access. |
stopNotificationService() |
Calls requestUnbind() while connected (API 24+); returns false if not bound or on API < 24. |
disconnect() |
Cancels the plugin's EventChannel subscription and clears the native sink (events buffer until you listen to notifications again). |
isNotificationServiceRunning() |
Returns true if the service is currently active. |
forceReconnect() |
Crucial: Toggles the component state to force Android to restart the service (fixes "silent kills"). |
openNotificationSettings() |
Opens Android's Notification Access settings screen. |
hasPermission() |
Checks if Notification Access is granted. |
notifications |
A Stream |
sendReply(notification obj, message) |
Sends a reply to a notification (requires the notification object and message). |
๐ Example Notification Payload
{
"id": "0|com.whatsapp|1|...",
"packageName": "com.whatsapp",
"title": "John Doe",
"text": "Hello world!",
"receivedAt": "2025-11-20 09:38:27",
"canReply": true,
"actions": [
{
"title": "Reply",
"inputs": ["reply_key"]
}
]
}
๐ง Notes
- Android Only: This plugin works on Android API 21+.
- Lifecycle: Android aggressively kills background services. The forceReconnect() method and the health monitor pattern shown in the example are highly recommended to ensure your app stays connected.
- Permissions: You cannot programmatically grant Notification Access. You must direct the user to the settings screen using openNotificationSettings().