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

9.1 KiB

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.