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:
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 (
|
from .ha_adb_common import (
|
||||||
AdbClientBase,
|
AdbClientBase,
|
||||||
MediaSessionInfo, # noqa: F401 (re-export)
|
MediaSessionInfo, # noqa: F401 (re-export)
|
||||||
|
parse_foreground,
|
||||||
)
|
)
|
||||||
from .ha_adb_common import ensure_adb_key as _ensure_adb_key
|
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.
|
prefer its media session over stale sessions from other apps.
|
||||||
"""
|
"""
|
||||||
return await self.poll_media_sessions(foreground_package)
|
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)
|
||||||
@@ -36,6 +36,7 @@ from .const import (
|
|||||||
ADB_RECONNECT_BACKOFF_START,
|
ADB_RECONNECT_BACKOFF_START,
|
||||||
ADB_RECONNECT_JITTER,
|
ADB_RECONNECT_JITTER,
|
||||||
STATE_STORE_TTL,
|
STATE_STORE_TTL,
|
||||||
|
SYSTEM_MEDIA_PACKAGES,
|
||||||
)
|
)
|
||||||
from .ha_adb_common import MediaSessionInfo, parse_audio_playback
|
from .ha_adb_common import MediaSessionInfo, parse_audio_playback
|
||||||
|
|
||||||
@@ -53,11 +54,21 @@ FIELD_IS_AVAILABLE = "is_available"
|
|||||||
FIELD_CURRENT_APP = "current_app"
|
FIELD_CURRENT_APP = "current_app"
|
||||||
FIELD_VOLUME_INFO = "volume_info"
|
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
|
# ADB-derived fields (cleared when the ADB connection goes down — they are
|
||||||
# regenerated by the watchdog); protocol pushes (IS_ON/IS_AVAILABLE/
|
# regenerated by the watchdog); protocol pushes (IS_ON/IS_AVAILABLE/
|
||||||
# CURRENT_APP/VOLUME_INFO) are device-reported truth and expire by TTL only,
|
# CURRENT_APP/VOLUME_INFO) are device-reported truth and expire by TTL only,
|
||||||
# so an ADB outage never clobbers them.
|
# 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 = (
|
PROTOCOL_FIELDS = (
|
||||||
FIELD_IS_ON,
|
FIELD_IS_ON,
|
||||||
FIELD_IS_AVAILABLE,
|
FIELD_IS_AVAILABLE,
|
||||||
@@ -371,6 +382,12 @@ class ShieldConnectionManager:
|
|||||||
"""Protocol current-app push (raw package; name resolution deferred)."""
|
"""Protocol current-app push (raw package; name resolution deferred)."""
|
||||||
return self.store.get(FIELD_CURRENT_APP)
|
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
|
@property
|
||||||
def volume_info(self) -> tuple | None:
|
def volume_info(self) -> tuple | None:
|
||||||
"""Protocol volume push as ``(level, max, muted)`` — readout only
|
"""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.
|
# Full dump, no truncation (B4) — low sessions must not be lost.
|
||||||
await poller.poll_media_sessions(head_lines=None)
|
await poller.poll_media_sessions(head_lines=None)
|
||||||
self.store.set(FIELD_SESSIONS, list(poller.all_sessions))
|
self.store.set(FIELD_SESSIONS, list(poller.all_sessions))
|
||||||
self.store.set(
|
# Zero-cost hint from the same dump; system processes filtered
|
||||||
FIELD_AUDIO_PLAYBACK, parse_audio_playback(poller.last_dump or "")
|
# (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())
|
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:
|
else:
|
||||||
# Standby: playback data is not current — expire it.
|
# Standby: playback data is not current — expire it.
|
||||||
self.store.set(FIELD_SESSIONS, [])
|
self.store.set(FIELD_SESSIONS, [])
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class FakePoller:
|
|||||||
self.sessions: list[MediaSessionInfo] = []
|
self.sessions: list[MediaSessionInfo] = []
|
||||||
self.apps: list[dict] = []
|
self.apps: list[dict] = []
|
||||||
self.audio_dump = ""
|
self.audio_dump = ""
|
||||||
|
self.foreground: str | None = None
|
||||||
|
self.foreground_calls = 0
|
||||||
self.head_lines_used: int | None = "unset"
|
self.head_lines_used: int | None = "unset"
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -76,6 +78,10 @@ class FakePoller:
|
|||||||
async def poll_running_apps(self) -> list[dict]:
|
async def poll_running_apps(self) -> list[dict]:
|
||||||
return list(self.apps)
|
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:
|
async def send_media_command(self, command: str) -> bool:
|
||||||
return self.connected
|
return self.connected
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user