"""Live events push (P5-3 / C-STREAM): logcat foreground trigger. Parses Android logcat's ActivityTaskManager ``START`` records and extracts the ``cmp=package`` part so the connection manager can stamp the foreground AppState instantly (sub-heartbeat latency). Logcat carries lifecycle only — no PlaybackState — so the hybrid split (ADB stays the playback/scene state source) is untouched: the stream never reads playback state. """ import re _LOG_RECORD_RE = re.compile(r"^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+") _CMP_RE = re.compile(r"cmp=([\w.]+)(?:/[^\s,}]+)?") _START_HINT_RE = re.compile(r"ActivityTaskManager:.*?START\b") def start_records(lines: list[str]) -> list[str]: """Split raw logcat lines into START-record buffers (wrapped lines joined). A record opens at a line that matches a log timestamp and mentions ActivityTaskManager + START; subsequent lines that do NOT look like a fresh log record are treated as continuations (the intent JSON can wrap across lines). Anything else closes/open the next record. """ records: list[str] = [] current: list[str] = [] for line in lines: if _LOG_RECORD_RE.match(line) and _START_HINT_RE.search(line): if current: records.append("\n".join(current)) current = [line] elif current and not _LOG_RECORD_RE.match(line): current.append(line) # wrapped intent text else: if current: records.append("\n".join(current)) current = [] if current: records.append("\n".join(current)) return records def foreground_from_logcat(lines: list[str]) -> str | None: """The foreground package from the LAST valid START record, or None. Non-START lines are ignored entirely; records whose intent carries no ``cmp=`` (e.g. service starts) yield nothing; a explicit ``null`` cmp is skipped too. Multi-user prefixes (``START u0``/``u10``...) are tolerated — the user prefix is never part of the cmp value. """ package: str | None = None for record in start_records(lines): match = _CMP_RE.search(record) if not match: continue candidate = match.group(1) if candidate and candidate != "null": package = candidate # last valid START wins return package def foreground_packages(lines: list[str]) -> list[str]: """Every distinct valid START package in order (test seam).""" seen: list[str] = [] for record in start_records(lines): match = _CMP_RE.search(record) if not match: continue candidate = match.group(1) if candidate and candidate != "null" and candidate not in seen: seen.append(candidate) return seen __all__ = [ "foreground_from_logcat", "foreground_packages", "start_records", ]