Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

192 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Mobile interface — touch, gestures, and hardware keys via ADB, backed by a Transport."""
from __future__ import annotations
import asyncio
from cua_sandbox.transport.base import Transport
class Mobile:
"""Mobile (Android) touch and hardware-key control.
All touch coordinates are in screen pixels. Single-touch methods use
``input tap/swipe`` via ``adb shell``. Multi-touch gestures delegate to
the ``multitouch_gesture`` transport action, which uses ``adb root`` +
MT Protocol B ``sendevent`` for reliable injection on both local and cloud
transports.
"""
def __init__(self, transport: Transport):
self._t = transport
async def _shell(self, command: str) -> None:
await self._t.send("shell", command=command)
# ── Touch ─────────────────────────────────────────────────────────────
async def tap(self, x: int, y: int) -> None:
await self._shell(f"input tap {x} {y}")
async def long_press(self, x: int, y: int, duration_ms: int = 1000) -> None:
await self._shell(f"input swipe {x} {y} {x} {y} {duration_ms}")
async def double_tap(self, x: int, y: int, delay: float = 0.1) -> None:
await self._shell(f"input tap {x} {y}")
await asyncio.sleep(delay)
await self._shell(f"input tap {x} {y}")
async def type_text(self, text: str) -> None:
encoded = text.replace(" ", "%s")
await self._shell(f"input text '{encoded}'")
# ── Gestures ──────────────────────────────────────────────────────────
async def swipe(self, x1: int, y1: int, x2: int, y2: int, duration_ms: int = 300) -> None:
await self._shell(f"input swipe {x1} {y1} {x2} {y2} {duration_ms}")
async def scroll_up(self, x: int, y: int, distance: int = 600, duration_ms: int = 400) -> None:
await self._shell(f"input swipe {x} {y + distance} {x} {y} {duration_ms}")
async def scroll_down(
self, x: int, y: int, distance: int = 600, duration_ms: int = 400
) -> None:
await self._shell(f"input swipe {x} {y} {x} {y + distance} {duration_ms}")
async def scroll_left(
self, x: int, y: int, distance: int = 400, duration_ms: int = 300
) -> None:
await self._shell(f"input swipe {x + distance} {y} {x} {y} {duration_ms}")
async def scroll_right(
self, x: int, y: int, distance: int = 400, duration_ms: int = 300
) -> None:
await self._shell(f"input swipe {x} {y} {x + distance} {y} {duration_ms}")
async def fling(self, x1: int, y1: int, x2: int, y2: int) -> None:
await self.swipe(x1, y1, x2, y2, duration_ms=80)
async def _get_screen_size(self) -> tuple[int, int]:
"""Return ``(width, height)`` in screen pixels via ``wm size``."""
result = await self._t.send("shell", command="wm size")
output = result.get("stdout", "").strip()
for line in output.splitlines():
if "size" in line.lower():
dims = line.split(":")[-1].strip()
w, h = dims.split("x")
return int(w), int(h)
return 32768, 32768 # fallback: 1:1 mapping
async def gesture(
self,
*finger_paths: tuple[int, int],
duration_ms: int = 400,
steps: int = 0,
) -> None:
"""Inject an arbitrary N-finger gesture via MT Protocol B ``sendevent``.
Each positional argument is a sequence of ``(x, y)`` waypoints for one
finger. Pass an even number of ``(x, y)`` tuples and they are paired
into start/end positions per finger::
# two-finger pinch-out
await mobile.gesture(
(cx - 20, cy), (cx - 200, cy), # finger 0
(cx + 20, cy), (cx + 200, cy), # finger 1
)
Delegates to the ``multitouch_gesture`` transport action, which uses
``adb root`` + MT Protocol B ``sendevent`` for reliable injection on
both local ADB and cloud HTTP transports.
Args:
*finger_paths: Alternating start/end ``(x, y)`` tuples, two per finger.
Must have an even length >= 4 (at least 2 fingers × 2 points).
duration_ms: Total gesture duration in milliseconds.
steps: Interpolation steps (0 = auto: duration_ms // 20, min 5).
"""
if len(finger_paths) < 4 or len(finger_paths) % 2 != 0:
raise ValueError(
"gesture() requires an even number of (x, y) tuples, two per finger "
f"(start + end). Got {len(finger_paths)} tuples."
)
# Pair waypoints into per-finger (start, end) tuples
fingers_pairs: list[tuple[tuple[int, int], tuple[int, int]]] = []
for i in range(0, len(finger_paths), 2):
fingers_pairs.append((finger_paths[i], finger_paths[i + 1]))
sw, sh = await self._get_screen_size()
n_steps = steps if steps > 0 else max(5, duration_ms // 20)
# Structured params for the server-side multitouch_gesture action
fingers_payload = [{"start": list(start), "end": list(end)} for start, end in fingers_pairs]
await self._t.send(
"multitouch_gesture",
fingers=fingers_payload,
screen_w=sw,
screen_h=sh,
duration_ms=duration_ms,
steps=n_steps,
)
async def pinch_in(self, cx: int, cy: int, spread: int = 300, duration_ms: int = 400) -> None:
"""Pinch-in (zoom out) with two real simultaneous fingers."""
await self.gesture(
(cx - spread, cy),
(cx - 20, cy),
(cx + spread, cy),
(cx + 20, cy),
duration_ms=duration_ms,
)
async def pinch_out(self, cx: int, cy: int, spread: int = 300, duration_ms: int = 400) -> None:
"""Pinch-out (zoom in) with two real simultaneous fingers."""
await self.gesture(
(cx - 20, cy),
(cx - spread, cy),
(cx + 20, cy),
(cx + spread, cy),
duration_ms=duration_ms,
)
# ── Hardware keys ─────────────────────────────────────────────────────
async def key(self, keycode: int) -> None:
await self._shell(f"input keyevent {keycode}")
async def home(self) -> None:
await self.key(3)
async def back(self) -> None:
await self.key(4)
async def recents(self) -> None:
await self.key(187)
async def power(self) -> None:
await self.key(26)
async def volume_up(self) -> None:
await self.key(24)
async def volume_down(self) -> None:
await self.key(25)
async def enter(self) -> None:
await self.key(66)
async def backspace(self) -> None:
await self.key(67)
# ── System ────────────────────────────────────────────────────────────
async def wake(self) -> None:
await self.key(224)
async def notifications(self) -> None:
await self._shell("service call statusbar 1")
async def close_notifications(self) -> None:
await self._shell("service call statusbar 2")