t59 F1 (HIGH). `ShieldAdbPoller.shell` swallows its error and answers None
(adb_client.py:144-155), and `_fetch_app_label` read that as "the dump
ran and found no label" → `cache_app_label(pkg, None)` → `('', stamp,
resolved=True)`. That is a DEFINITIVE negative for APP_LABEL_TTL = 24 h,
so after ONE transient failure `app_label()` never re-enqueued, exactly
one `pm dump` was ever attempted, and a later good dump could not
recover the label — `sensor.native_value` and `media_player.app_name`
stayed on the package id for every later render.
The dump now carries an exit marker (`; echo __NSH_EXIT:$?`), so the
three outcomes are distinguishable:
* output with an `application-label` → cache the label;
* output that ran and carried none (the marker is still there) → cache
the empty string, the definitive negative that keeps t24 F1's
zero-round-trip rule for a genuinely label-less package;
* no output at all (`None`, or blank) → cache NOTHING and let the next
render re-enqueue, logged at debug.
The `resolved` flag goes with it: `cache_app_label` was its only writer
and always passed True, so an entry in `_app_labels` is now simply "a
fetch RAN" and the dead third field (`(label, stamp, True)`) is gone.
Tests drive the FakePoller-returns-None AND returns-'' shapes through
the sensor and through `media_player.app_name`, plus the label-less
package that must still be negatively cached. The stub poller now models
both real outcomes of a dump (an answer, or a transport that delivered
nothing) instead of conflating them — which is why the old tests could
only pin the raising-shell branch that already worked. 4 of the new
tests fail on the pre-fix tree, showing `('', stamp, True)` stamped.
Also in this commit:
- t59 F3: running_apps.py set `app["state"] = "paused"` twice — one line.
- t59 F4: no captured fixture contains state=6, so the "the Shield's own
sessions report 6 like stock Android" wording in README and the
media_session/media_player docstrings is reworded as INFERRED from the
`state=PlaybackState {state=N, …}` line shape. A `git notes` entry on
07682ff records the same correction against the t55 commit body.
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Build the running apps list from recent tasks + media sessions.
|
|
|
|
Produces the media_dag_running_apps attribute value that media_zone reads.
|
|
Recent tasks (from dumpsys activity recents) provide the list of running apps.
|
|
Media sessions provide playback state for apps that have active sessions.
|
|
"""
|
|
|
|
from .media_session import MediaSessionInfo
|
|
|
|
|
|
def running_apps_from_tasks(
|
|
recent_tasks: list[dict],
|
|
media_sessions: list[MediaSessionInfo] | None = None,
|
|
foreground_package: str | None = None,
|
|
last_known: dict[str, MediaSessionInfo] | None = None,
|
|
) -> list[dict] | None:
|
|
"""Build a list of running app dicts from recent tasks + media sessions.
|
|
|
|
Args:
|
|
recent_tasks: List of {"package": str} from ADB recent tasks.
|
|
media_sessions: Optional media sessions for playback state enrichment.
|
|
foreground_package: The current foreground app package.
|
|
last_known: Optional per-package TTL-filtered last-known-state cache
|
|
(P3-3b) — apps without a live session carry their last known
|
|
playback state as ``last_known_state``/``last_known_title``.
|
|
|
|
Returns None if there are fewer than 2 apps (no point showing a switcher
|
|
with only one option).
|
|
"""
|
|
if not recent_tasks or len(recent_tasks) < 2:
|
|
return None
|
|
|
|
# Index media sessions by package for quick lookup
|
|
session_by_pkg: dict[str, MediaSessionInfo] = {}
|
|
if media_sessions:
|
|
for s in media_sessions:
|
|
if s.package:
|
|
session_by_pkg[s.package] = s
|
|
|
|
apps = []
|
|
for task in recent_tasks:
|
|
pkg = task["package"]
|
|
app: dict = {"package": pkg}
|
|
|
|
# Mark foreground app
|
|
if pkg == foreground_package:
|
|
app["foreground"] = True
|
|
|
|
# Enrich with media session data if available
|
|
session = session_by_pkg.get(pkg)
|
|
if session:
|
|
if session.playing:
|
|
app["state"] = "playing"
|
|
elif session.buffering:
|
|
app["state"] = "buffering"
|
|
elif session.paused:
|
|
app["state"] = "paused"
|
|
else:
|
|
app["state"] = "idle"
|
|
if session.title:
|
|
app["title"] = session.title
|
|
if session.artist:
|
|
app["artist"] = session.artist
|
|
else:
|
|
app["state"] = "running"
|
|
# P3-3b: no live session — surface the TTL-capped last-known state
|
|
cached = (last_known or {}).get(pkg)
|
|
if cached is not None:
|
|
if cached.playing:
|
|
app["last_known_state"] = "playing"
|
|
elif cached.buffering:
|
|
app["last_known_state"] = "buffering"
|
|
elif cached.paused:
|
|
app["last_known_state"] = "paused"
|
|
if cached.title:
|
|
app["last_known_title"] = cached.title
|
|
|
|
apps.append(app)
|
|
|
|
return apps if len(apps) >= 2 else None
|
|
|
|
|
|
# Keep backward compat for any code still using this
|
|
def running_apps_from_sessions(sessions: list[MediaSessionInfo]) -> list[dict] | None:
|
|
"""Build running apps from media sessions only (legacy)."""
|
|
if not sessions:
|
|
return None
|
|
tasks = [{"package": s.package} for s in sessions if s.package]
|
|
return running_apps_from_tasks(tasks, sessions)
|