80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""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",
|
|
]
|