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 False
|
||||||
return await self._poller.bring_to_foreground(package)
|
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 ──
|
# ── Lifecycle ──
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
async def async_start(self) -> None:
|
||||||
|
|||||||
@@ -54,6 +54,44 @@ JELLYFIN_APP_IDS = [
|
|||||||
"com.github.damontecres.wholphin",
|
"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
|
# Verified on-device package names (docs/RESEARCH_SHIELD.md): TiviMate does
|
||||||
# not publish a MediaSession on Shield Android 11 — it is the headline
|
# not publish a MediaSession on Shield Android 11 — it is the headline
|
||||||
# Phase-3 fallback app.
|
# Phase-3 fallback app.
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ class AdbClientBase:
|
|||||||
return True
|
return True
|
||||||
return False
|
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:
|
async def bring_to_foreground(self, package: str) -> bool:
|
||||||
"""Bring an app to the foreground using monkey command."""
|
"""Bring an app to the foreground using monkey command."""
|
||||||
result = await self.shell(
|
result = await self.shell(
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ class PlaybackIntent:
|
|||||||
the remote that did not serve it.
|
the remote that did not serve it.
|
||||||
adb_command: The ADB keycode command name matching the intent
|
adb_command: The ADB keycode command name matching the intent
|
||||||
(play/pause/stop/next/previous); set together with via_adb.
|
(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")
|
target_state: str # MediaPlayerState value (e.g. "playing", "paused")
|
||||||
command: str
|
command: str
|
||||||
created_at: float
|
created_at: float
|
||||||
@@ -31,6 +34,8 @@ class PlaybackIntent:
|
|||||||
retry_count: int = 0
|
retry_count: int = 0
|
||||||
via_adb: bool = False
|
via_adb: bool = False
|
||||||
adb_command: str | None = None
|
adb_command: str | None = None
|
||||||
|
via_protocol: bool = False
|
||||||
|
protocol_key: str | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
|
|||||||
Reference in new issue
Block a user