Files
agentic-mobile-control/openspec/changes/android-driver/design.md
T

54 lines
9.1 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.
- **Exact Appium UiAutomator2 mobile-command names/parameters for gesture-based methods (`tap`, `swipe`, `home`) are confirmed against the installed `Appium-Python-Client` source/docs at implementation time, not guessed here.** 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, which exposes its own gesture command set (e.g. click/drag/swipe gesture commands) with different parameter shapes. Implementation must read the installed driver's actual capability before writing each method body — this is called out as its own task rather than assumed. `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 need no research.
- **`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
- [Appium UiAutomator2 mobile-command names for tap/swipe/home are not yet confirmed] → Mitigation: dedicated implementation task to read the installed `Appium-Python-Client` UiAutomator2 driver source/docs before writing these three method bodies; not treated as settled by this design doc.
- [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)`), verifying exact UiAutomator2 mobile-command names/parameters against the installed Appium client before implementing `tap`/`swipe`/`home`.
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
- Exact UiAutomator2 mobile-command names/parameters for `tap`/`swipe`/`home` — resolved during implementation (task-level, not a proposal-level blocker).
- Exact capability key name for the system-port equivalent (`systemPort` or similar) — confirmed against `UiAutomator2Options` during implementation.