feat(android): add native controls and continuous multi-touch

This commit is contained in:
chenmingxuan committed 2026-11-02 21:33:45 +08:00
1 parent 3dd96078db
commit 4b12ae7728
70 files changed
+13105 -179

No files matched your search

+53
View File
@@ -113,6 +113,59 @@ class AndroidAdbDriver(BaseDeviceDriver):
def screen_size(self) -> tuple[int, int]:
return (self._width, self._height)
@property
def native_capabilities(self) -> frozenset[str]:
capabilities = getattr(self._ui_adb_client, "native_capabilities", frozenset())
return capabilities if isinstance(capabilities, frozenset) else frozenset()
async def native_request(
self, command: str, args: dict[str, Any], *, timeout: float | None = None
) -> dict[str, Any]:
request = getattr(self._ui_adb_client, "native_request", None)
if not callable(request):
return await super().native_request(command, args, timeout=timeout)
operation = asyncio.create_task(asyncio.to_thread(request, command, args, timeout=timeout))
try:
return await asyncio.shield(operation)
except asyncio.CancelledError:
if command == "inspect_control":
# A read cannot inject input. Do not let a blocked synchronous
# transport or session repair consume the condition's deadline.
operation.cancel()
raise
# Keep the resource locked until the bounded native call finishes. Cancelling
# to_thread alone would leave input running after its task had released it.
if command == "touch_gesture" and args.get("operation_id"):
cancellation = asyncio.create_task(
asyncio.to_thread(
request,
"cancel_gesture",
{"operation_id": args["operation_id"]},
timeout=2.0,
)
)
while not cancellation.done():
try:
await asyncio.shield(cancellation)
except asyncio.CancelledError:
continue
except (OSError, RuntimeError, ValueError) as exc:
logger.debug(f"Native gesture cancellation failed during cleanup: {exc}")
break
while not operation.done():
try:
await asyncio.shield(operation)
except asyncio.CancelledError:
continue
except (OSError, RuntimeError, ValueError):
break
if not operation.cancelled():
try:
operation.result()
except (OSError, RuntimeError, ValueError) as exc:
logger.debug(f"Native operation failed while draining cancelled input: {exc}")
raise
@property
def device(self) -> AdbDevice:
if self._device is None:
+14
View File
@@ -68,6 +68,20 @@ class ScreenData(BaseModel):
class BaseDeviceDriver(ABC):
"""Abstract Base Class for mobile device and emulator drivers."""
@property
def native_capabilities(self) -> frozenset[str]:
return frozenset()
async def native_request(
self, command: str, args: dict[str, Any], *, timeout: float | None = None
) -> dict[str, Any]:
return {
"success": False,
"code": "UNSUPPORTED",
"execution": "not_started",
"error": "This device backend does not implement native Android actions.",
}
@property
@abstractmethod
def device_id(self) -> str: