feat(ha_adb_common): foreground parser — mResumedActivity primary with block-join for ActivityRecord wraps, topResumedActivity fallback (R4); no window-focus parsing

This commit is contained in:
James Mackay committed 2026-10-08 10:32:39 +01:00
1 parent 34d09e9f70
commit f1e49a661b
2 files changed
+93

No files matched your search

@@ -0,0 +1,91 @@
"""Foreground-app parsing from `dumpsys activity activities` (RESEARCH_SHIELD R4).
Primary pattern: ``mResumedActivity: ActivityRecord{... u0 <pkg>/<activity> ...}``
(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 <pkg>/<activity>` — 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