92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""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
|