Always-on Android companion that captures notifications (and optional SMS) on your device and forwards them to a configured HTTP endpoint. Built with Flutter + minimal Kotlin.
- Foreground service keeps the app alive; auto-starts after boot
- Notification capture via
NotificationListenerService
- Optional full SMS body capture via
ContentObserver
(READ_SMS) - Endpoint forwarding (JSON): reception, message_body, message_from, message_date
- App filtering: pick which apps to forward (with icons, search, runtime cache)
- Deduplication of repeated notifications/SMS
- Robust delivery: events bridged to Dart; native HTTP fallback if Dart not ready; retry queue with exponential backoff
- Persistent settings: reception, endpoint, allowed packages, SMS toggle; queue persists across restarts
- Logs: in-app screen plus mirrored to logcat (
tag: MsgMirror
), with copy/clear/auto refresh - Queue viewer: in-app screen to review pending, unsent items; includes a Force Retry action
Declared in android/app/src/main/AndroidManifest.xml
:
INTERNET
– send HTTP requestsPOST_NOTIFICATIONS
(Android 13+)ACCESS_NETWORK_STATE
– read Data Saver status for UX and guidanceFOREGROUND_SERVICE
– run foreground serviceREAD_SMS
– optional, only if enabling SMS observerRECEIVE_BOOT_COMPLETED
– auto start service at bootQUERY_ALL_PACKAGES
– broad visibility (OEMs). Also explicit queries for:com.google.android.apps.messaging
com.google.android.dialer
Registered components:
- Service
AlwaysOnService
– foreground, hosts headless Flutter engine - Service
MsgNotificationListener
–NotificationListenerService
- Receiver
BootReceiver
– startsAlwaysOnService
after boot - Receiver
NotifEventReceiver
– bridges broadcasts → Dart channel
- System posts a notification →
MsgNotificationListener.onNotificationPosted
- Listener filters (allowed packages, skip ongoing/group summaries) and logs
- Listener sends payload via:
- Broadcast (
lol.arian.notifmirror.NOTIF_EVENT
) →NotifEventReceiver
→ Dart channel - Direct channel call if channel is already ready
- Native HTTP fallback (ApiSender) if channel unavailable
- Broadcast (
- Dart (
MessageStream
) receives event on channel, formats payload, dedups, sends to endpoint - Logs recorded throughout (native + Dart; also to logcat)
{
"message_body": "Hello world",
"message_from": "Arian",
"message_date": "2025-09-03 19:00",
"app": "com.google.android.apps.messaging",
"type": "notification"
}
Payload notes:
- By default,
app
andtype
are included (type isnotification
orsms
). reception
is included when configured in settings.- The payload template supports placeholders:
{{body}}
,{{from}}
,{{date}}
,{{app}}
,{{type}}
,{{reception}}
. - Additional notification fields are captured and can be used in templates (if present on the device/notification):
{{title}}
,{{text}}
,{{when}}
,{{isGroupSummary}}
,{{subText}}
,{{summaryText}}
,{{bigText}}
,{{infoText}}
,{{people}}
,{{category}}
,{{priority}}
,{{channelId}}
,{{actions}}
,{{groupKey}}
,{{visibility}}
,{{color}}
,{{badgeIconType}}
,{{largeIcon}}
(base64 PNG),{{picture}}
(base64 PNG).
lib/main.dart
- App UI (settings, permissions, background service controls)
- AppBar action to open full-screen Logs
- Background entrypoint
backgroundMain
for headless engine
lib/message_stream.dart
- MethodChannel receiver (
msg_mirror
) - Builds payloads, deduplicates, filters by allowed packages
- Sends JSON to configured endpoint; detailed logging
- MethodChannel receiver (
lib/prefs.dart
- Platform channel (
msg_mirror_prefs
) helpers: get/set reception, endpoint
- Platform channel (
lib/permissions.dart
- Permissions bridge for notification access, post notifications, read SMS, battery optimizations
lib/logger.dart
- Logs bridge (
msg_mirror_logs
) with append/read/clear
- Logs bridge (
lib/app_selector.dart
- Select installed apps with icons, search, filter “only selected”, runtime cache
lib/logs_screen.dart
- Full-screen logs viewer with refresh/auto/clear/copy
QueueScreen
to view pending retry items
android/app/src/main/kotlin/.../AlwaysOnService.kt
- Foreground service; initializes FlutterEngine
- Creates channels before running Dart; caches engine as
always_on_engine
- Registers SMS observer if enabled and permission granted
.../MsgNotificationListener.kt
NotificationListenerService
that filters and emits notifications- Broadcasts events and also attempts channel delivery; native HTTP fallback via
ApiSender
- Logs lifecycle (
onCreate
,onListenerConnected
, etc.)
.../NotifEventReceiver.kt
- Receives broadcasted notification payloads and forwards to Dart channel
- Chooses UI engine channel if present; falls back to background engine
.../BootReceiver.kt
- Starts
AlwaysOnService
on boot
- Starts
.../MainActivity.kt
- Exposes channels:
msg_mirror_ctrl
: start/stop service,isServiceRunning
msg_mirror_prefs
: get/set reception, endpoint, SMS toggle, allowed packages, retry queue (JSON)msg_mirror_perm
: permissions checks and settings intents; Data Saver status (getDataSaverStatus
) and settings shortcutmsg_mirror_logs
: append/read/clear logsmsg_mirror_apps
: list installed apps and fetch icons
- Caches UI engine in
ui_engine
for receivers
- Exposes channels:
.../SmsObserver.kt
- Optional SMS inbox observer; posts SMS payload to Dart
.../LogStore.kt
- File-backed log with rotation; mirrors to logcat (tag
MsgMirror
)
- File-backed log with rotation; mirrors to logcat (tag
.../ApiSender.kt
- Native HTTP POST fallback (reads endpoint/reception from SharedPreferences)
- Build/install the app (debug or release)
- Open app and configure:
- Reception and Endpoint → Save destination
- Select apps → choose which packages to forward
- Permissions: grant Notification Access, Post Notifications, (optional) Read SMS
- Background Service: Start service (UI shows checking state on launch); whitelist from battery optimizations
- Data Saver: either turn OFF globally, or keep ON and enable Unrestricted data for the app
- Trigger a test notification from a selected app
- Check Logs (in-app Logs screen or logcat tag
MsgMirror
)
- Auto-start after reboot via
BootReceiver
(some OEMs require whitelist) - Foreground service is
START_STICKY
and restarts after process kills - Data Saver status mapping (Android): 1=Disabled (OK), 2=Whitelisted (OK), 3=Enabled (restricts background; not OK)
- Deduping:
- Notifications: key =
app|whenMs
- SMS: key =
sms|from|dateMs
- Notifications: key =
- Skips:
- Group summaries, ongoing/system background entries
- Self-app notifications
- Empty bodies (falls back to title if needed)
- Package visibility:
QUERY_ALL_PACKAGES
plus explicit queries for Google Messages/Dialer
- No events in Logs:
- Ensure Notification Access is enabled (toggle OFF/ON if needed)
- Start the foreground service
- Check
MsgMirror
tag in logcat forMsgListener onListenerConnected
- Events logged but no POST:
- Verify Endpoint and Reception; see
POST done: status=...
or error - Some OEMs block background networking without whitelist
- Ensure Data Saver is OFF or app is whitelisted (Unrestricted data)
- Verify Endpoint and Reception; see
- If your webhook becomes available after a period of downtime and you want to immediately drain the queue, open the Queue screen and tap “Force Retry”. This triggers an immediate attempt and logs the action in the Logs screen.
- Failed sends are queued in a persistent retry queue and retried with exponential backoff up to 60 seconds between attempts.
- The queue is restored on app/service restart and retries resume automatically.
- Use the Queue screen’s “Force Retry” button to trigger an immediate pass over the queue (continues scheduling if items remain).
- SMS not forwarding:
- Enable “Enable SMS observer” and grant READ_SMS
- Some devices restrict SMS access
- Clean build if UI doesn’t reflect changes:
flutter clean && rm -rf android/.gradle android/build build .dart_tool && flutter pub get
- Explicitly target entrypoint:
flutter build apk --release -t lib/main.dart
- Data stays on your device until forwarded to your endpoint
- Be mindful of forwarding other parties’ messages to third-party services