""" Notification Monitor Daemon Runs in Termux with root. Polls `dumpsys notification` and POSTs new notifications to EventServer at http://127.0.0.1:8765. Usage: python notification_monitor.py [--interval 2] [--verbose] """ import re import json import time import hashlib import argparse import subprocess import sys import urllib.request from dataclasses import dataclass, field from typing import Optional EVENT_SERVER = "http://127.0.0.1:8765" POLL_INTERVAL = 2 # seconds # Packages to ignore (system noise) IGNORED_PKGS = { "android", "com.android.systemui", "com.google.android.gms", "com.android.phone", "com.google.android.permissioncontroller", } @dataclass class NotifRecord: pkg: str notif_id: str key: str title: str = "" text: str = "" ranking_time_ms: int = 0 def stable_hash(self) -> str: """Dedup key: key + title + text (catches updated notifications).""" raw = f"{self.key}|{self.title}|{self.text}" return hashlib.md5(raw.encode()).hexdigest() def run_dumpsys() -> str: result = subprocess.run( ["su", "-c", "dumpsys notification --noredact"], capture_output=True, text=True, timeout=10 ) return result.stdout def parse_notifications(output: str) -> list[NotifRecord]: records: list[NotifRecord] = [] current: Optional[NotifRecord] = None # Match NotificationRecord header line header_re = re.compile( r"NotificationRecord\([^:]+:\s*pkg=(\S+)\s+.*?id=(\d+)\s.*?key=([^\s:]+)" ) key_re = re.compile(r"^\s+key=(-?\d+\|[^\s]+)") title_re = re.compile(r"android\.title=String \((.+?)\)\s*$") text_re = re.compile(r"android\.text=String \((.+?)\)\s*$") time_re = re.compile(r"mRankingTimeMs=(\d+)") for line in output.splitlines(): m = header_re.search(line) if m: if current: records.append(current) pkg, notif_id, key = m.group(1), m.group(2), m.group(3) current = NotifRecord(pkg=pkg, notif_id=notif_id, key=key) continue if current is None: continue if m := key_re.match(line): current.key = m.group(1) elif m := title_re.search(line): current.title = m.group(1) elif m := text_re.search(line): current.text = m.group(1) elif m := time_re.search(line): current.ranking_time_ms = int(m.group(1)) if current: records.append(current) return [r for r in records if r.pkg not in IGNORED_PKGS and r.title] def post_event(notif: NotifRecord, verbose: bool = False): payload = json.dumps({ "type": "notification", "app": notif.pkg, "data": { "id": notif.notif_id, "key": notif.key, "title": notif.title, "text": notif.text, "ts_ms": notif.ranking_time_ms, } }).encode() req = urllib.request.Request( f"{EVENT_SERVER}/events", data=payload, headers={"Content-Type": "application/json"}, method="POST" ) try: with urllib.request.urlopen(req, timeout=3) as r: resp = json.loads(r.read().decode()) if verbose: print(f"[+] {notif.pkg}: {notif.title!r} → event #{resp['id']}", flush=True) except Exception as e: print(f"[!] POST failed: {e}", file=sys.stderr, flush=True) def wait_for_server(timeout=30): deadline = time.time() + timeout while time.time() < deadline: try: urllib.request.urlopen(f"{EVENT_SERVER}/health", timeout=2) return True except Exception: time.sleep(1) return False def main(): parser = argparse.ArgumentParser() parser.add_argument("--interval", type=float, default=POLL_INTERVAL) parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() print("[notification_monitor] Waiting for EventServer...", flush=True) if not wait_for_server(): print("[!] EventServer not reachable at", EVENT_SERVER, file=sys.stderr) sys.exit(1) print(f"[notification_monitor] Connected. Polling every {args.interval}s", flush=True) seen: set[str] = set() while True: try: output = run_dumpsys() records = parse_notifications(output) new_records = [r for r in records if r.stable_hash() not in seen] for r in new_records: seen.add(r.stable_hash()) post_event(r, verbose=args.verbose) # Bound seen set size (keep last 2000 hashes) if len(seen) > 2000: seen = set(list(seen)[-1000:]) except subprocess.TimeoutExpired: print("[!] dumpsys timed out", file=sys.stderr, flush=True) except KeyboardInterrupt: print("\n[notification_monitor] Stopped.", flush=True) break except Exception as e: print(f"[!] Error: {e}", file=sys.stderr, flush=True) time.sleep(args.interval) if __name__ == "__main__": main()