Files
agentic-mobile-control/openspec/changes/android-driver/design.md
T
q792602257 62923b9285 docs(openspec): confirm android-driver UiAutomator2 commands via research
Resolve the previously-open design questions in the android-driver
change by researching the appium-uiautomator2-driver docs and Appium 3
release notes:

- tap -> mobile: clickGesture
- swipe -> mobile: dragGesture (duration_ms converted to speed px/s)
- home -> mobile: pressKey (KEYCODE_HOME)
- port isolation -> appium:systemPort capability
- Appium 3 breaking changes confirmed to not affect this design

Updates design.md (Decisions/Risks/Open Questions/Migration Plan) and
tasks.md (section 1 and tasks 2.1/2.4/4.1) accordingly.
2026-07-13 13:56:04 +08:00

60 lines
11 KiB
Markdown

## Context
`driver/base.py::Driver` is the driver-independent ABC (connect/disconnect/screenshot/tap/swipe/input/launch/terminate/tree/home/lock/unlock). `driver/wda_driver.py::WDADriver` is the only implementation today: it wraps the Appium Python Client's `XCUITestOptions`, builds a `webdriver.Remote` session against an Appium server, and wraps every operation's exceptions into `core.errors.DriverError`/`DeviceOfflineError`. `driver/registry.py::SUPPORTED_DRIVER_TYPES` maps a `driver_type` string (today only `"wda"`) to a builder that strips out the driver's own config fields from a `connection_info` dict and routes the rest into `extra_capabilities`.
`openspec/specs/driver-registry/spec.md` already documents this mechanism as driver-type-agnostic: "Adding a driver type requires no changes outside the driver layer." Grepping `api/console.py` and `api/rest.py` confirms neither contains a `"wda"` literal — the API layer resolves `driver_type` purely through the registry, so this change is expected to be additive to `driver/` alone.
This is exactly the case the project's Hexagonal + DDD governance decision calls out: `core`/`driver`/`device`/`tools` must stay free of LLM/HTTP-framework/MCP dependencies, and every new driver's design must state how it preserves that boundary. `docs/MACOS_IPHONE_SETUP.md` §1 already documents Android as the known, not-yet-registered gap this change closes.
## Goals / Non-Goals
**Goals:**
- Add a second real `Driver` implementation (`AndroidDriver`, Appium UiAutomator2) proving the registry extension point generalizes beyond WDA.
- Keep the change additive: no edits to `api/`, `device/`, `tools/`, or `runtime/`.
- Bring the new driver's test coverage above the existing WDA bar (mocked unit tests, not just a hardware-gated integration test).
**Non-Goals:**
- A full Android SDK/adb/real-device setup guide (`docs/ANDROID_SETUP.md` or equivalent) — deferred to a follow-up change once the driver can be validated against real hardware (user-confirmed scope decision).
- Espresso driver support — UiAutomator2 only, mirroring WDA's XCUITest-only scope.
- Renaming `APEX_WDA_*` env vars — an unrelated, already-documented follow-up item in `docs/MACOS_IPHONE_SETUP.md` §12.
- Any change to `Driver`'s abstract interface — the existing method set is sufficient; Android does not need new capabilities the ABC doesn't already express.
## Decisions
- **Registry key is `"uiautomator2"`, not `"android"`.** The existing key `"wda"` names the automation *backend* (WebDriverAgent), not the platform (`"ios"`). For the two supported driver types to stay consistent, Android's key should likewise name its backend — Appium's UiAutomator2 driver — not its platform. Rejected alternative: `"android"`, which would break that symmetry and would also be ambiguous if Espresso support is ever added later (both would be "android").
- **`AndroidDriverConfig` mirrors `WDADriverConfig` field-for-field where an Android equivalent exists**: `server_url` (same default `http://127.0.0.1:4723` — one Appium server can host sessions for both platforms), `platform_name="Android"`, `automation_name="UiAutomator2"`, `device_name`, `udid` (adb serial, selects among multiple connected devices), `no_reset`, `extra_capabilities`. `wda_local_port` (WDA's per-session port-isolation capability for parallel devices) has a direct UiAutomator2 analog — a system-port capability serving the same purpose — carried over as `system_port`. Rejected alternative: a from-scratch config shape — rejected because the parity makes both drivers predictable to configure from the same `connection_info` dict shape the registry already handles generically.
- **Same error-wrapping pattern as `WDADriver`**: every method's Appium/network exception is caught and re-raised as the existing `DriverError`, with `DeviceOfflineError` reserved for connect failures and pre-connect calls (via the same `_require_client()` guard pattern). Rejected alternative: introducing Android-specific error types — rejected because `core/errors.py`'s existing hierarchy is already driver-agnostic and callers above the driver layer must not need to know which concrete driver raised.
- **Appium UiAutomator2 mobile-command names/parameters for gesture-based methods (`tap`, `swipe`, `home`), confirmed via research against the official `appium-uiautomator2-driver` docs (2026-07)**: WDA's `tap`/`swipe`/`home` use WDA-specific `mobile:` command names (`mobile: tap`, `mobile: dragFromToForDuration`, `mobile: pressButton`) that do not carry over verbatim to UiAutomator2. The confirmed Android equivalents:
- `tap(x, y)``execute_script("mobile: clickGesture", {"x": x, "y": y})`. The driver's own docs recommend this over any legacy tap call as a workaround for native-tap failures, so it is also the more robust choice, not just the closest analog.
- `swipe(start_x, start_y, end_x, end_y, duration_ms)``execute_script("mobile: dragGesture", {"startX": start_x, "startY": start_y, "endX": end_x, "endY": end_y, "speed": speed})`. Unlike WDA's `dragFromToForDuration`, `dragGesture` takes a `speed` in pixels/second instead of a duration, so the implementation must convert: `speed = distance / (duration_ms / 1000)`, guarding against a zero/near-zero distance (fall back to the driver's default speed rather than dividing by zero). `mobile: swipeGesture` was considered and rejected — it takes a bounding-area + direction + percent shape, not a coordinate pair, so it does not match `Driver.swipe`'s signature.
- `home()``execute_script("mobile: pressKey", {"keycode": 3})` (Android `KeyEvent.KEYCODE_HOME`).
- The parallel-session port-isolation capability is confirmed as `appium:systemPort` (maps directly to `AndroidDriverConfig.system_port`), documented by the driver as "recommended for parallel tests" — the direct analog of WDA's `wdaLocalPort`.
- `screenshot`/`tree`/`input`/`launch`/`terminate`/`lock`/`unlock` map to the same cross-platform Selenium/Appium client methods WDA already uses (`get_screenshot_as_png`, `page_source`, `switch_to.active_element.send_keys`, `activate_app`, `terminate_app`, `lock`, `unlock`) and needed no research.
- Implementation should still sanity-check these against whatever `appium-uiautomator2-driver` version is actually resolved in `.venv` at coding time (docs reflect the driver's current released behavior as of this research, not a pinned version in this repo).
- **Appium 3 compatibility, confirmed via research (released 2025-08-07, latest 3.5.2 as of this research)**: Appium 3 is a deliberately small breaking-change release (Node.js/npm minimum version bump, mandatory feature-flag scope prefixes for `--allow-insecure`, `GET /sessions` moved to `GET /appium/sessions` behind a feature flag, full JSONWP removal in favor of W3C-only parameters, driver-owned file upload handling). None of these affect this change: `AndroidDriver` (like `WDADriver`) builds sessions purely through W3C `Options` classes, never relies on session discovery, requests no insecure feature flags, and does no file upload. `Appium-Python-Client>=5.1.1` (already pinned in root `pyproject.toml`) has no reported incompatibility with Appium 3 servers. No version pin changes are needed.
- **`driver-registry` spec gets a new scenario, not a new capability file.** No `wda-driver` capability spec exists today — the registry mechanism is spec'd once, generically, and individual driver behavior is not separately spec'd. Adding an `android-driver` capability would break that precedent for no benefit; instead, `driver-registry`'s existing "Building a factory for a known driver type" requirement gets a second scenario for `driver_type="uiautomator2"`, mirroring the existing `"wda"` scenario, so the spec's example coverage stays symmetric across both real driver types.
- **Test coverage exceeds current WDA parity on purpose.** `WDADriver` today has zero mocked unit tests — only `tests/test_wda_integration.py`, hardware-gated and skipped without `APEX_WDA_*` env vars. For `AndroidDriver`, add mocked unit tests (mock `appium.webdriver.Remote`) covering connect failure → `DeviceOfflineError`, pre-connect calls → `DeviceOfflineError`, and operation exceptions → `DriverError`, in addition to an equivalent hardware-gated `tests/test_android_integration.py`. This is a deliberate quality bar increase, not scope creep — it costs nothing extra in production code and closes a gap the WDA driver has always had.
- **No new setup documentation in this change.** Writing an accurate, detailed Android SDK/adb/real-device guide (parallel to `docs/MACOS_IPHONE_SETUP.md`'s 12 sections) without access to real hardware to validate each step would mean inventing untested instructions — the existing iOS doc reads as having been validated against a real device. `docs/MACOS_IPHONE_SETUP.md` §1 gets only a factual correction (Android driver is now registered); a full setup guide is explicit follow-up work once real-device validation is possible.
## Risks / Trade-offs
- [`dragGesture`'s `speed` (px/s) is a different shape from `Driver.swipe`'s `duration_ms`] → Mitigation: convert explicitly (`speed = distance / (duration_ms / 1000)`) with a guard for zero/near-zero distance; cover this conversion with a unit test (start==end and a normal case) rather than trusting it silently.
- [No real Android device or emulator available during this change to exercise `connect()`/`screenshot()` end-to-end] → Mitigation: mocked unit tests cover the error-handling contract; the integration test is env-var gated and simply skips until real hardware is available, exactly mirroring `WDADriver`'s current state — no regression versus today's validation depth.
- [`driver-registry` spec now carries two platform-specific example scenarios under one requirement] → Mitigation: accepted; this is the intended pattern for a third driver type in the future, not a maintenance burden.
## Migration Plan
1. Add `driver/android_driver.py` (`AndroidDriverConfig`, `AndroidDriver(Driver)`), using the confirmed UiAutomator2 mobile commands for `tap`/`swipe`/`home` (see Decisions), with a quick sanity check against whatever driver version is actually resolved in `.venv`.
2. Add `build_android_driver_factory` and register `SUPPORTED_DRIVER_TYPES["uiautomator2"]` in `driver/registry.py`.
3. Add mocked unit tests for `AndroidDriver` (connect failure, pre-connect calls, per-method exception wrapping).
4. Add `tests/test_android_integration.py`, mirroring `tests/test_wda_integration.py`'s env-var-gated structure.
5. Add the `driver-registry` spec delta scenario for `driver_type="uiautomator2"`.
6. Correct `docs/MACOS_IPHONE_SETUP.md` §1's now-outdated "Android not registered" statement.
7. Run the full non-integration test suite (`uv run --all-packages pytest -m "not integration"`) and confirm no regressions.
Rollback: everything is additive (one new driver module, one new registry entry, new test files, a one-paragraph doc correction). No data/schema migration. Rollback is deleting the new files and reverting the doc line.
## Open Questions
None outstanding. Both items originally listed here (exact UiAutomator2 mobile-command names/parameters for `tap`/`swipe`/`home`, and the system-port capability's exact key name) were resolved via research against the official `appium-uiautomator2-driver` docs — see the Decisions section.