diff --git a/custom_components/nvidia_shield/adb_session.py b/custom_components/nvidia_shield/adb_session.py index 9cb9303..db92fb2 100644 --- a/custom_components/nvidia_shield/adb_session.py +++ b/custom_components/nvidia_shield/adb_session.py @@ -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) diff --git a/custom_components/nvidia_shield/connection.py b/custom_components/nvidia_shield/connection.py index c153ed3..262fa5e 100644 --- a/custom_components/nvidia_shield/connection.py +++ b/custom_components/nvidia_shield/connection.py @@ -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, []) diff --git a/tests/test_connection.py b/tests/test_connection.py index 8131182..7d0feac 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -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