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
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}}
.
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
- 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