feat(connection): ADB foreground fallback on the watchdog cadence when the protocol push is silent (store priority protocol > ADB); audio-playback hint filtered through SYSTEM_MEDIA_PACKAGES (P3-2b,c)

This commit is contained in:
James Mackay committed 2026-10-08 10:33:38 +01:00
1 parent f1e49a661b
commit 8885ddd5b5
3 files changed
+51 -4

No files matched your search

@@ -16,6 +16,7 @@ import logging
from .ha_adb_common import (
AdbClientBase,
MediaSessionInfo, # noqa: F401 (re-export)
parse_foreground,
)
from .ha_adb_common import ensure_adb_key as _ensure_adb_key
@@ -43,3 +44,13 @@ class AdbMediaSessionPoller(AdbClientBase):
prefer its media session over stale sessions from other apps.
"""
return await self.poll_media_sessions(foreground_package)
async def poll_foreground(self) -> str | None:
"""Foreground package via `dumpsys activity activities` (RESEARCH R4).
The grep keeps the dump tiny; the parser handles the R4 line wrap.
"""
dump = await self.shell("dumpsys activity activities 2>/dev/null | grep -iE 'resumed'")
if not dump:
return None
return parse_foreground(dump)
+34 -4
View File
@@ -36,6 +36,7 @@ from .const import (
ADB_RECONNECT_BACKOFF_START,
ADB_RECONNECT_JITTER,
STATE_STORE_TTL,
SYSTEM_MEDIA_PACKAGES,
)
from .ha_adb_common import MediaSessionInfo, parse_audio_playback
@@ -53,11 +54,21 @@ FIELD_IS_AVAILABLE = "is_available"
FIELD_CURRENT_APP = "current_app"
FIELD_VOLUME_INFO = "volume_info"
# ADB fallback foreground identity (P3-2): stamped only when the protocol
# push is silent; consumers prefer FIELD_CURRENT_APP over this.
FIELD_FOREGROUND = "foreground"
# ADB-derived fields (cleared when the ADB connection goes down — they are
# regenerated by the watchdog); protocol pushes (IS_ON/IS_AVAILABLE/
# CURRENT_APP/VOLUME_INFO) are device-reported truth and expire by TTL only,
# so an ADB outage never clobbers them.
ADB_FIELDS = (FIELD_AWAKE, FIELD_SESSIONS, FIELD_AUDIO_PLAYBACK, FIELD_RUNNING_APPS)
ADB_FIELDS = (
FIELD_AWAKE,
FIELD_SESSIONS,
FIELD_AUDIO_PLAYBACK,
FIELD_RUNNING_APPS,
FIELD_FOREGROUND,
)
PROTOCOL_FIELDS = (
FIELD_IS_ON,
FIELD_IS_AVAILABLE,
@@ -371,6 +382,12 @@ class ShieldConnectionManager:
"""Protocol current-app push (raw package; name resolution deferred)."""
return self.store.get(FIELD_CURRENT_APP)
@property
def foreground_app(self) -> str | None:
"""Best foreground identity: protocol push first, ADB parse fallback
(P3-2 store priority: FIELD_CURRENT_APP > FIELD_FOREGROUND)."""
return self.store.get(FIELD_CURRENT_APP) or self.store.get(FIELD_FOREGROUND)
@property
def volume_info(self) -> tuple | None:
"""Protocol volume push as ``(level, max, muted)`` — readout only
@@ -598,10 +615,23 @@ class ShieldConnectionManager:
# Full dump, no truncation (B4) — low sessions must not be lost.
await poller.poll_media_sessions(head_lines=None)
self.store.set(FIELD_SESSIONS, list(poller.all_sessions))
self.store.set(
FIELD_AUDIO_PLAYBACK, parse_audio_playback(poller.last_dump or "")
)
# Zero-cost hint from the same dump; system processes filtered
# (mediashell/systemui/tv settings/diagtools report "audio").
playback = [
pkg
for pkg in parse_audio_playback(poller.last_dump or "")
if pkg not in SYSTEM_MEDIA_PACKAGES
]
self.store.set(FIELD_AUDIO_PLAYBACK, playback)
self.store.set(FIELD_RUNNING_APPS, await poller.poll_running_apps())
# Foreground fallback (P3-2): ADB parse only when the protocol
# push is silent — store priority is protocol > ADB, so this
# never overwrites a live push.
if self.store.get(FIELD_CURRENT_APP) is None:
foreground = await poller.poll_foreground()
if foreground:
self.store.set(FIELD_FOREGROUND, foreground)
self._cache_last_known(foreground)
else:
# Standby: playback data is not current — expire it.
self.store.set(FIELD_SESSIONS, [])
+6
View File
@@ -39,6 +39,8 @@ class FakePoller:
self.sessions: list[MediaSessionInfo] = []
self.apps: list[dict] = []
self.audio_dump = ""
self.foreground: str | None = None
self.foreground_calls = 0
self.head_lines_used: int | None = "unset"
async def connect(self) -> bool:
@@ -76,6 +78,10 @@ class FakePoller:
async def poll_running_apps(self) -> list[dict]:
return list(self.apps)
async def poll_foreground(self) -> str | None:
self.foreground_calls += 1
return self.foreground
async def send_media_command(self, command: str) -> bool:
return self.connected