feat: protocol key/launch command layer — RemoteKeyCode maps, C-LAUNCH app launch, ADB digit keyevent fallback, PlaybackIntent protocol path
This commit is contained in:
1 parent
906126b0da
commit
61aca9d413
4 files changed
+92
-1
No files matched your search
@@ -403,6 +403,44 @@ class ShieldConnectionManager:
|
||||
return False
|
||||
return await self._poller.bring_to_foreground(package)
|
||||
|
||||
# -- Protocol command delegation (Phase 2) --
|
||||
|
||||
async def send_protocol_key(self, key: str, direction: str = "SHORT") -> bool:
|
||||
"""Send a remote-protocol key push (fast, non-blocking).
|
||||
|
||||
Returns False when the transport is down or the send raises — entities
|
||||
then fall back to the ADB keyevent path.
|
||||
"""
|
||||
if self._protocol is None:
|
||||
return False
|
||||
try:
|
||||
self._protocol.send_key_command(key, direction)
|
||||
return True
|
||||
except Exception as err:
|
||||
_LOGGER.debug("Protocol key %s failed: %s", key, err)
|
||||
return False
|
||||
|
||||
async def launch_app(self, app_link_or_app_id: str) -> bool:
|
||||
"""Launch an app or URL over the protocol transport.
|
||||
|
||||
``send_launch_app_command`` auto-prefixes ``market://launch?id=`` for
|
||||
bare package ids (C-LAUNCH); deep links pass through unchanged.
|
||||
"""
|
||||
if self._protocol is None:
|
||||
return False
|
||||
try:
|
||||
self._protocol.send_launch_app_command(app_link_or_app_id)
|
||||
return True
|
||||
except Exception as err:
|
||||
_LOGGER.debug("Protocol launch %s failed: %s", app_link_or_app_id, err)
|
||||
return False
|
||||
|
||||
async def send_digit_key(self, digit: str) -> bool:
|
||||
"""Channel digit via the ADB keyevent path (fallback for §7)."""
|
||||
if not self.available:
|
||||
return False
|
||||
return await self._poller.send_digit_keyevent(digit)
|
||||
|
||||
# ── Lifecycle ──
|
||||
|
||||
async def async_start(self) -> None:
|
||||
|
||||
@@ -54,6 +54,44 @@ JELLYFIN_APP_IDS = [
|
||||
"com.github.damontecres.wholphin",
|
||||
]
|
||||
|
||||
# Remote-protocol key names (RemoteKeyCode enum in remotemessage.proto).
|
||||
# HA service kind → protocol keycode string.
|
||||
PROTOCOL_MEDIA_KEYS = {
|
||||
"media_play": "KEYCODE_MEDIA_PLAY",
|
||||
"media_pause": "KEYCODE_MEDIA_PAUSE",
|
||||
"media_stop": "KEYCODE_MEDIA_STOP",
|
||||
"media_next_track": "KEYCODE_MEDIA_NEXT",
|
||||
"media_previous_track": "KEYCODE_MEDIA_PREVIOUS",
|
||||
}
|
||||
PROTOCOL_VOLUME_KEYS = {
|
||||
"volume_up": "KEYCODE_VOLUME_UP",
|
||||
"volume_down": "KEYCODE_VOLUME_DOWN",
|
||||
"volume_mute": "KEYCODE_VOLUME_MUTE",
|
||||
}
|
||||
PROTOCOL_REMOTE_KEYS = {
|
||||
**PROTOCOL_VOLUME_KEYS,
|
||||
"play": "KEYCODE_MEDIA_PLAY",
|
||||
"pause": "KEYCODE_MEDIA_PAUSE",
|
||||
"stop": "KEYCODE_MEDIA_STOP",
|
||||
"up": "KEYCODE_DPAD_UP",
|
||||
"down": "KEYCODE_DPAD_DOWN",
|
||||
"left": "KEYCODE_DPAD_LEFT",
|
||||
"right": "KEYCODE_DPAD_RIGHT",
|
||||
"enter": "KEYCODE_DPAD_CENTER",
|
||||
"home": "KEYCODE_HOME",
|
||||
"back": "KEYCODE_BACK",
|
||||
"menu": "KEYCODE_MENU",
|
||||
"search": "KEYCODE_SEARCH",
|
||||
"power": "KEYCODE_POWER",
|
||||
"channel_up": "KEYCODE_CHANNEL_UP",
|
||||
"channel_down": "KEYCODE_CHANNEL_DOWN",
|
||||
}
|
||||
|
||||
# §7 channel selection: digits only, ~0.1 s apart, no ENTER by default
|
||||
# (replicates HA core androidtv_remote's verified behavior).
|
||||
CHANNEL_DIGIT_DELAY = 0.1
|
||||
CONF_CHANNEL_SEND_ENTER = "channel_send_enter"
|
||||
|
||||
# Verified on-device package names (docs/RESEARCH_SHIELD.md): TiviMate does
|
||||
# not publish a MediaSession on Shield Android 11 — it is the headline
|
||||
# Phase-3 fallback app.
|
||||
|
||||
@@ -164,6 +164,16 @@ class AdbClientBase:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def send_digit_keyevent(self, digit: str) -> bool:
|
||||
"""Send a channel digit via `input keyevent` (KEYCODE_0..9 = 7..16)."""
|
||||
if not digit.isdigit() or len(digit) != 1:
|
||||
_LOGGER.warning("Not a single digit: %s", digit)
|
||||
return False
|
||||
result = await self.shell(f"input keyevent {7 + int(digit)}")
|
||||
if result is not None:
|
||||
_LOGGER.debug("ADB digit keyevent %d (%s)", 7 + int(digit), digit)
|
||||
return result is not None
|
||||
|
||||
async def bring_to_foreground(self, package: str) -> bool:
|
||||
"""Bring an app to the foreground using monkey command."""
|
||||
result = await self.shell(
|
||||
|
||||
@@ -22,8 +22,11 @@ class PlaybackIntent:
|
||||
the remote that did not serve it.
|
||||
adb_command: The ADB keycode command name matching the intent
|
||||
(play/pause/stop/next/previous); set together with via_adb.
|
||||
via_protocol: True when the command was served by the remote-protocol
|
||||
key push (Phase 2) — the retry then goes over the protocol key
|
||||
again instead of the other transports.
|
||||
protocol_key: The protocol keycode string served with via_protocol.
|
||||
"""
|
||||
|
||||
target_state: str # MediaPlayerState value (e.g. "playing", "paused")
|
||||
command: str
|
||||
created_at: float
|
||||
@@ -31,6 +34,8 @@ class PlaybackIntent:
|
||||
retry_count: int = 0
|
||||
via_adb: bool = False
|
||||
adb_command: str | None = None
|
||||
via_protocol: bool = False
|
||||
protocol_key: str | None = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
|
||||
Reference in new issue
Block a user