feat(android): add native controls and continuous multi-touch
This commit is contained in:
1 parent
3dd96078db
commit
4b12ae7728
70 files changed
+13105
-179
No files matched your search
@@ -38,12 +38,15 @@ from __future__ import annotations
|
||||
from io import BytesIO
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from artemis.runtime.native_protocol import input_completion_seconds
|
||||
|
||||
from artemis.clients.ui_automator_client import (
|
||||
UIAutomatorScreenData,
|
||||
_parse_hierarchy_xml_to_elements,
|
||||
@@ -215,6 +218,85 @@ class AccessibilityClient:
|
||||
return False
|
||||
return self._manager.ping(session.local_port) is not None
|
||||
|
||||
@property
|
||||
def native_capabilities(self) -> frozenset[str]:
|
||||
"""Advertised at attach time; capability queries never block on network IO."""
|
||||
return frozenset(self._session.capabilities) if self._session is not None else frozenset()
|
||||
|
||||
def native_request(
|
||||
self, command: str, args: dict[str, Any], *, timeout: float | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Preserve native receipts. A failed POST is never sent to another backend."""
|
||||
read_only = command == "inspect_control"
|
||||
if command == "inspect_control" and self._session is None:
|
||||
try:
|
||||
self._ensure_session()
|
||||
except (HelperUnavailable, OSError, RuntimeError) as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"code": "UNAVAILABLE",
|
||||
"execution": "not_started",
|
||||
"error": str(exc),
|
||||
}
|
||||
session = self._session
|
||||
if session is None:
|
||||
return {
|
||||
"success": False,
|
||||
"code": "UNAVAILABLE",
|
||||
"execution": "not_started",
|
||||
"error": "Connect the Android helper before issuing native operations.",
|
||||
}
|
||||
if (
|
||||
command not in {"inspect_control", "control_action", "touch_gesture", "cancel_gesture"}
|
||||
or command not in session.capabilities
|
||||
):
|
||||
return {
|
||||
"success": False,
|
||||
"code": "UNSUPPORTED",
|
||||
"execution": "not_started",
|
||||
"error": "The attached Android helper does not advertise this native action. Reconnect after upgrading the bundled helper.",
|
||||
}
|
||||
completion_window = input_completion_seconds(command, args)
|
||||
started = time.monotonic()
|
||||
request_timeout = max(timeout or self._request_timeout, completion_window)
|
||||
|
||||
def drain_uncertain_input() -> None:
|
||||
remaining = completion_window - (time.monotonic() - started)
|
||||
if remaining > 0:
|
||||
time.sleep(remaining)
|
||||
|
||||
try:
|
||||
result = json.loads(
|
||||
self._http_once(
|
||||
session, "/action", {**args, "cmd": command}, request_timeout
|
||||
).decode("utf-8")
|
||||
)
|
||||
if not isinstance(result, dict) or not isinstance(result.get("success"), bool):
|
||||
raise ValueError("Malformed native action receipt.")
|
||||
if result.get("execution") not in {"not_started", "observed", "completed"}:
|
||||
drain_uncertain_input()
|
||||
return result
|
||||
except (
|
||||
HelperUnavailable,
|
||||
HelperRequestError,
|
||||
urllib.error.URLError,
|
||||
OSError,
|
||||
ValueError,
|
||||
TimeoutError,
|
||||
) as exc:
|
||||
status = (
|
||||
exc.status if isinstance(exc, HelperRequestError) else getattr(exc, "code", None)
|
||||
)
|
||||
rejected = status in {400, 401, 403, 404, 405, 413, 422, 429}
|
||||
if not read_only and not rejected:
|
||||
drain_uncertain_input()
|
||||
return {
|
||||
"success": False,
|
||||
"code": "UNAVAILABLE" if read_only else "TRANSPORT_ERROR",
|
||||
"execution": "not_started" if read_only or rejected else "unknown",
|
||||
"error": f"{exc}. Observe current state; do not replay an unacknowledged write.",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Transport
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
Reference in new issue
Block a user