"""Foreground-app parsing from `dumpsys activity activities` (RESEARCH_SHIELD R4). Primary pattern: ``mResumedActivity: ActivityRecord{... u0 / ...}`` (Shield Android 11). ``topResumedActivity`` (newer firmware) is the fallback, checked only after the primary — per R4 finding 1. ``dumpsys window`` focus is NOT parsed (R4 finding 2: the first ``mCurrentFocus`` can be literally null). Multi-line ActivityRecord wraps are handled by joining the block until the record closes or a state line arrives (R4 finding 3: match on the joined block, not per-line). ``ResumedActivity``/``Resumed`` duplicates are tolerated. """ import re _RESUMED_MARKERS = ( ("mResumedActivity", "mResumedActivity:"), ("topResumedActivity", "topResumedActivity:"), ("ResumedActivity", "ResumedActivity:"), ("Resumed", "Resumed:"), ) # `u0 /` — the userId prefix tolerates u0..uN; the package is # anything up to the first `/` (or closing brace when the activity is bare). _PACKAGE_RE = re.compile(r"u\d+\s+(\S+?)\s*/|u\d+\s+(\S+?)\s*}") def _marker_kind(stripped: str) -> str | None: for kind, marker in _RESUMED_MARKERS: if stripped.startswith(marker): return kind return None def _resumed_blocks(dump: str) -> list[tuple[str, str]]: """(kind, joined-text) blocks for each resumed-activity line, in order.""" blocks: list[tuple[str, str]] = [] kind: str | None = None buf = "" for raw in dump.split("\n"): stripped = raw.strip() marker = _marker_kind(stripped) if marker is not None: if buf: blocks.append((kind, buf)) kind, buf = marker, stripped continue if kind is None: continue # Continuation of a wrapped ActivityRecord: join until the record # closes (brace) or the state line completes the block (R4 finding 3). buf += " " + stripped if "}" in stripped or "state=" in stripped: blocks.append((kind, buf)) kind, buf = None, "" if buf: blocks.append((kind, buf)) return blocks def _extract_package(text: str) -> str | None: match = _PACKAGE_RE.search(text) if match is None: return None package = match.group(1) or match.group(2) if not package or package in ("null", "Window{"): return None return package def parse_foreground(dump: str) -> str | None: """Foreground package: mResumedActivity primary, topResumed fallback. Returns None when neither pattern matches (e.g. pre-parse wrappers) — callers must not fall back to window-focus parsing (R4 finding 2). """ if not dump: return None blocks = _resumed_blocks(dump) for kind, text in blocks: if kind == "topResumedActivity": continue package = _extract_package(text) if package: return package for kind, text in blocks: if kind != "topResumedActivity": continue package = _extract_package(text) if package: return package return None