android-driver 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. android_driver-0.0.1/.gitignore +11 -0
  2. android_driver-0.0.1/CONTRIBUTING.md +92 -0
  3. android_driver-0.0.1/LICENSE +21 -0
  4. android_driver-0.0.1/PKG-INFO +270 -0
  5. android_driver-0.0.1/README.md +240 -0
  6. android_driver-0.0.1/docs/agent-guide.md +66 -0
  7. android_driver-0.0.1/docs/configuration.md +135 -0
  8. android_driver-0.0.1/docs/installation.md +117 -0
  9. android_driver-0.0.1/docs/recipes.md +153 -0
  10. android_driver-0.0.1/docs/roadmap.md +57 -0
  11. android_driver-0.0.1/examples/README.md +36 -0
  12. android_driver-0.0.1/examples/compose-app/.android-driver.yaml +77 -0
  13. android_driver-0.0.1/examples/view-app/.android-driver.yaml +61 -0
  14. android_driver-0.0.1/pyproject.toml +66 -0
  15. android_driver-0.0.1/src/android_driver/__init__.py +3 -0
  16. android_driver-0.0.1/src/android_driver/actions.py +209 -0
  17. android_driver-0.0.1/src/android_driver/adb.py +355 -0
  18. android_driver-0.0.1/src/android_driver/build.py +81 -0
  19. android_driver-0.0.1/src/android_driver/config.py +211 -0
  20. android_driver-0.0.1/src/android_driver/drivers/__init__.py +22 -0
  21. android_driver-0.0.1/src/android_driver/drivers/adb_driver.py +104 -0
  22. android_driver-0.0.1/src/android_driver/drivers/base.py +146 -0
  23. android_driver-0.0.1/src/android_driver/drivers/factory.py +29 -0
  24. android_driver-0.0.1/src/android_driver/drivers/u2_driver.py +100 -0
  25. android_driver-0.0.1/src/android_driver/emulator.py +277 -0
  26. android_driver-0.0.1/src/android_driver/expect.py +190 -0
  27. android_driver-0.0.1/src/android_driver/log.py +16 -0
  28. android_driver-0.0.1/src/android_driver/recipes.py +547 -0
  29. android_driver-0.0.1/src/android_driver/record.py +112 -0
  30. android_driver-0.0.1/src/android_driver/run.py +292 -0
  31. android_driver-0.0.1/src/android_driver/scan.py +155 -0
  32. android_driver-0.0.1/src/android_driver/server.py +777 -0
  33. android_driver-0.0.1/src/android_driver/session.py +144 -0
  34. android_driver-0.0.1/src/android_driver/ui.py +261 -0
  35. android_driver-0.0.1/tests/__init__.py +0 -0
  36. android_driver-0.0.1/tests/conftest.py +107 -0
  37. android_driver-0.0.1/tests/fixtures/home_screen.xml +8 -0
  38. android_driver-0.0.1/tests/fixtures/login_screen.xml +15 -0
  39. android_driver-0.0.1/tests/integration/__init__.py +0 -0
  40. android_driver-0.0.1/tests/integration/conftest.py +42 -0
  41. android_driver-0.0.1/tests/integration/test_live.py +192 -0
  42. android_driver-0.0.1/tests/test_actions.py +85 -0
  43. android_driver-0.0.1/tests/test_config.py +88 -0
  44. android_driver-0.0.1/tests/test_expect.py +120 -0
  45. android_driver-0.0.1/tests/test_packaging.py +86 -0
  46. android_driver-0.0.1/tests/test_recipes.py +244 -0
  47. android_driver-0.0.1/tests/test_reload.py +113 -0
  48. android_driver-0.0.1/tests/test_run.py +103 -0
  49. android_driver-0.0.1/tests/test_scan.py +109 -0
  50. android_driver-0.0.1/tests/test_server.py +87 -0
  51. android_driver-0.0.1/tests/test_session.py +82 -0
  52. android_driver-0.0.1/tests/test_ui.py +81 -0
@@ -0,0 +1,11 @@
1
+ venv/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ runs/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .DS_Store
@@ -0,0 +1,92 @@
1
+ # Contributing
2
+
3
+ Bug reports, recipes for apps you have driven, and backend fixes are all welcome. The project is
4
+ early, so the shape of things is still open to argument.
5
+
6
+ ## Setup
7
+
8
+ ```bash
9
+ git clone https://github.com/earlzdev/android-driver.git
10
+ cd android-driver
11
+ uv sync --extra dev
12
+ ```
13
+
14
+ ```bash
15
+ uv run pytest # 129 unit tests, no device needed
16
+ uv run ruff check src tests
17
+ ```
18
+
19
+ The unit suite runs against a fake driver over recorded hierarchy fixtures in `tests/fixtures/`, so
20
+ the whole engine — selector resolution, recipes, assertions, run bundles, config discovery — is
21
+ testable without an emulator. Please keep it that way: a test that needs a device belongs in
22
+ `tests/integration/`.
23
+
24
+ ## Working on the server from Claude Code
25
+
26
+ Opening this repo in Claude Code gives you the tools built from your working tree, via a
27
+ project-scoped `.mcp.json`. Edit the source, restart the server, and the change is live.
28
+
29
+ Two things about that setup are load-bearing:
30
+
31
+ - The dev server is named `android-driver-dev`, not `android-driver`. If you also have the plugin
32
+ installed, two servers sharing a name makes it ambiguous which one a tool call reaches.
33
+ - The plugin's own MCP config is a separate file, `mcp-config.json`. A `.mcp.json` at a repo root is
34
+ read as a *project* config, where `${CLAUDE_PLUGIN_ROOT}` does not resolve.
35
+
36
+ **Never launch with `uvx --from <path>`.** uv keys that build cache on `pyproject.toml`'s mtime, so
37
+ editing anything under `src/` leaves the cached wheel in place and the server keeps serving old code
38
+ — silently, with a normal-looking startup. `uv run` re-syncs from source every start.
39
+ `tests/test_packaging.py` will fail if anyone reintroduces it, because nothing else would catch it.
40
+
41
+ ## The live suite
42
+
43
+ Needs a booted emulator, and is skipped otherwise:
44
+
45
+ ```bash
46
+ ANDROID_DRIVER_LIVE=1 uv run pytest tests/integration
47
+ ```
48
+
49
+ It drives [FlakyDemo](test_app/), the demo app in this repo. Build and install it first:
50
+
51
+ ```bash
52
+ cd test_app && ./gradlew :app:assembleDebug
53
+ ```
54
+
55
+ The live suite saves and loads snapshots and can leave the device in an unexpected state, so point it
56
+ at a scratch AVD rather than one you care about.
57
+
58
+ ## Testing a change end to end
59
+
60
+ Unit tests cannot see the two things most likely to break for a user: what the *shipped* launcher
61
+ runs, and what a real device does. Both have burned this project before. If you touch packaging,
62
+ `session.py`, `emulator.py` or the drivers, run the loop for real against FlakyDemo — install, smoke,
63
+ and one repro from a snapshot — before opening the PR.
64
+
65
+ ## Style
66
+
67
+ - Comments explain **why**, not what. If a line is there because of a device quirk, say which quirk —
68
+ that is the knowledge worth keeping.
69
+ - Errors should say what to do next. This is the standard to match:
70
+
71
+ ```
72
+ no adb device in state 'device'. Start an emulator with `start_emulator`,
73
+ or check `adb devices` for an unauthorized/offline entry.
74
+ ```
75
+
76
+ - Tool docstrings are read by an agent, not just by you. Write them for the reader who has to decide
77
+ whether this is the right tool.
78
+ - `ruff check` must pass. The project does not enforce `ruff format`.
79
+
80
+ ## Adding a tool
81
+
82
+ Tools live in `src/android_driver/server.py` and are thin: they wrap a function from `actions.py` or
83
+ `expect.py` in `_act`, which handles errors, timing, run recording and evidence capture. Put real
84
+ logic in the action layer, so recipes get it too — recipes and hand-driven tools share that path
85
+ deliberately, and a tool that bypasses it will drift.
86
+
87
+ Add the name to `RESERVED_TOOL_NAMES`, and a row to the tool table in `README.md`.
88
+
89
+ ## Pull requests
90
+
91
+ Say what broke and how you know it is fixed. A failing test that now passes is the best form of that;
92
+ a run directory from a real device is a good second.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 android-driver contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.5
2
+ Name: android-driver
3
+ Version: 0.0.1
4
+ Summary: MCP server that turns an Android emulator into a deterministic, agent-drivable test harness.
5
+ Project-URL: Homepage, https://github.com/earlzdev/android-driver
6
+ Project-URL: Repository, https://github.com/earlzdev/android-driver
7
+ Project-URL: Issues, https://github.com/earlzdev/android-driver/issues
8
+ Project-URL: Documentation, https://github.com/earlzdev/android-driver#readme
9
+ Author: earlzdev
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: adb,agent,android,emulator,mcp,testing,uiautomator
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: mcp<2,>=1.2
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: uiautomator2<4,>=3.2
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # android-driver
32
+
33
+ **Drive an Android emulator as a deterministic test harness — from Claude Code.**
34
+
35
+ [![CI](https://github.com/earlzdev/android-driver/actions/workflows/ci.yml/badge.svg)](https://github.com/earlzdev/android-driver/actions/workflows/ci.yml)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
+
39
+ You ask an agent to reproduce a bug in your Android app. It dumps 80 KB of accessibility XML into its
40
+ own context, taps something that turns out to be the wrong element, and when the bug does not appear
41
+ it cannot repeat what it just did — because the app is now three screens deep in a state nobody
42
+ recorded.
43
+
44
+ android-driver is built for that loop instead: **build → install → drive → assert → reset → repeat.**
45
+
46
+ ```
47
+ > reproduce the crash when the bio field goes over 100 characters
48
+
49
+ snapshot_load("clean") 1.9s
50
+ open_settings() ok
51
+ type_text(id=text_field_Bio, text=110 chars) ok
52
+ expect_log("StringIndexOutOfBoundsException") ok — matched
53
+ expect_no_crash() FAILED — 1 crash record
54
+
55
+ runs/20260901-080304-repro-set-06/report.md
56
+ → java.lang.StringIndexOutOfBoundsException: begin 0, end 120, length 110
57
+ at ...screens.SettingsScreenKt.SettingsScreen$textFields(SettingsScreen.kt:149)
58
+ ```
59
+
60
+ Three attempts from the same snapshot, three identical results, and a directory of evidence to point
61
+ at. That is the whole idea.
62
+
63
+ ---
64
+
65
+ ## Install
66
+
67
+ It is a Claude Code plugin: one install brings the tools, a skill that teaches Claude the loop, and
68
+ three slash commands.
69
+
70
+ ```bash
71
+ claude plugin marketplace add earlzdev/android-driver
72
+ claude plugin install android-driver@android-driver
73
+ ```
74
+
75
+ There is no venv to manage — the plugin builds the Python server from source on demand, and passes
76
+ your project directory to it so your config is found wherever Claude was launched from.
77
+
78
+ **Requirements:** `adb` and the Android SDK's `emulator` on `PATH`, Python ≥ 3.10. For the faster
79
+ uiautomator2 backend run `python -m uiautomator2 init` once per device; without it the server falls
80
+ back to a pure-adb backend that needs nothing installed on the device.
81
+
82
+ Prefer a plain MCP server, or not using Claude Code at all? See
83
+ [docs/installation.md](docs/installation.md).
84
+
85
+ ## Quickstart
86
+
87
+ ```
88
+ /android-driver:setup
89
+ ```
90
+
91
+ It checks your toolchain, finds your `applicationId` and build command, detects whether you are on
92
+ Compose or Views, writes a starter `.android-driver.yaml`, boots an emulator and proves the loop
93
+ works. Then:
94
+
95
+ ```
96
+ /android-driver:smoke # build, install, walk the main flows, assert nothing broke
97
+ /android-driver:repro <what is broken> # reproduce it from a snapshot and leave evidence
98
+ ```
99
+
100
+ Or just talk to it — the `android-testing` skill loads automatically when a task involves driving the
101
+ app, so "check that login still works on a fresh install" does the right thing without ceremony.
102
+
103
+ ## Why it works this way
104
+
105
+ Four decisions do most of the work.
106
+
107
+ **Snapshots, so a repro is actually reproducible.** `snapshot_save` freezes the emulator's exact
108
+ state; `snapshot_load` restores it and waits until the device is genuinely drivable again — 1.8–2.2s
109
+ measured on a Pixel 7 AVD. Reinstalling and re-navigating costs 30–90s *and drifts a little each
110
+ time*. An agent testing thirty variations of a hypothesis needs the cheap, identical option: the
111
+ difference between two attempts only means something if everything else was the same.
112
+
113
+ **A screen index instead of a wall of XML.** A raw `uiautomator dump` is 50–200 KB per screen — tens
114
+ of thousands of tokens for a model that just wants to know what it can tap. `screen` returns this:
115
+
116
+ ```
117
+ device=emulator-5554 app=com.example.app/.MainActivity screen=1080x2400 driver=uiautomator2
118
+ #5 [Scroll] id=settings_container (scrollable) @(540,1236)
119
+ #7 [Text] "Settings" id=homepage_title @(235,472)
120
+ #18 [EditText] "" id=text_field_Bio @(540,1018)
121
+ ```
122
+
123
+ Then `tap(ref="#7")`. Two orders of magnitude smaller, and it reads like a menu. The raw tree is
124
+ still there behind `dump_ui_xml` for when you genuinely need it.
125
+
126
+ **Assertions that collect their own evidence.** `expect_visible` polls, so it is safe immediately
127
+ after a tap and will not flake on an animation; when it fails it hands back the screen index that
128
+ *was* there, plus a screenshot and hierarchy dump on disk. `expect_no_crash` reads the `crash` buffer
129
+ as well as `main`, because a native abort never reaches `main` at all. Wrap a sequence in
130
+ `run_start` / `run_end` and you get `runs/<id>/` holding a timeline, a report, the logcat slice for
131
+ exactly that window, and every failure artifact — so an agent cites a directory instead of describing
132
+ what it saw.
133
+
134
+ **Your flows as first-class tools.** The six steps every test starts with — sign in, create an order,
135
+ join a call — go into `.android-driver.yaml` once and become real MCP tools with typed parameters. An
136
+ agent sees `login(email, password)` in its tool list rather than rediscovering the flow from a screen
137
+ dump every session. Recipes run the same code path as the hand-driven tools, so the two cannot drift.
138
+
139
+ <details>
140
+ <summary>Plus the device knowledge that costs an afternoon each to learn</summary>
141
+
142
+ - **Uninstall-then-install**, not `pm install -r` — debug APKs from different branches carry
143
+ different signing keys and otherwise fail with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
144
+ - **Check `mInputShown` before pressing Back**, so dismissing the keyboard never dismisses the
145
+ dialog behind it.
146
+ - **Write to Compose `TextField`s through the accessibility node**, not tap-then-type, which lands
147
+ text in the wrong field.
148
+ - **Settle after a click** before the next query, or you read pre-animation state.
149
+ - **An `appops` pass** for OEM permission overlays that keep blocking after `pm grant` reports
150
+ success.
151
+
152
+ </details>
153
+
154
+ ## Configure
155
+
156
+ `.android-driver.yaml` at your project root is what makes a generic tool specific to your app.
157
+ `/android-driver:setup` writes a starter for you.
158
+
159
+ ```yaml
160
+ app:
161
+ package: com.example.myapp
162
+ activity: .MainActivity # optional; the launcher intent is resolved otherwise
163
+
164
+ build:
165
+ command: ./gradlew :app:assembleDebug
166
+ apk_glob: app/build/outputs/apk/debug/*.apk
167
+
168
+ driver:
169
+ backend: auto # auto | uiautomator2 | adb
170
+
171
+ selectors: # scanned, so a typo is a warning rather than a mystery
172
+ sources: ["app/src/main/**/*.kt"]
173
+
174
+ recipes: # each becomes an MCP tool with typed parameters
175
+ login:
176
+ params: {email: {required: true}, password: {required: true, secret: true}}
177
+ steps:
178
+ - launch:
179
+ - type: {desc: text_field_Email, text: "{{email}}"}
180
+ - tap: {desc: login_button}
181
+ - expect_visible: {desc: home_greeting, timeout_s: 20}
182
+ ```
183
+
184
+ The file is optional: with no config every generic tool still works — you pass `pkg=` explicitly and
185
+ lose `build_app` and recipes. It is found by walking up from your project and then, failing that, up
186
+ to three levels *down*, so an app in `app/` or `android/` is discovered without configuration.
187
+
188
+ Full reference: **[docs/configuration.md](docs/configuration.md)** · recipe and step syntax:
189
+ **[docs/recipes.md](docs/recipes.md)** · worked examples for Compose and View projects:
190
+ [`examples/`](examples/).
191
+
192
+ ## Tools
193
+
194
+ 45, plus one per configured recipe.
195
+
196
+ | Group | Tools |
197
+ |---|---|
198
+ | Emulator | `list_avds` `start_emulator` `stop_emulator` `wait_for_boot` `snapshot_save` `snapshot_load` `snapshot_list` `snapshot_delete` |
199
+ | Device | `list_devices` `select_device` `device_info` |
200
+ | App | `build_app` `install_app` `uninstall_app` `app_info` `launch_app` `force_stop` `clear_app_data` |
201
+ | UI | `screen` `tap` `tap_xy` `long_press` `type_text` `swipe` `scroll_to` `press_key` `screenshot` `dump_ui_xml` |
202
+ | Assertions | `expect_visible` `expect_gone` `expect_log` `expect_no_crash` |
203
+ | Runs | `run_start` `run_end` `run_list` `record_start` `record_stop` |
204
+ | Recipes | `list_recipes` `run_recipe` `check_recipes` `list_selectors` `reload_config` |
205
+ | Logs | `logcat_clear` `logcat_read` |
206
+ | Shell | `shell` |
207
+
208
+ > [!WARNING]
209
+ > `shell` is unrestricted on purpose — this is a development tool, not a sandbox. It can wipe device
210
+ > data, kill processes and read files. Point it at emulators and test devices, not at anything you
211
+ > care about.
212
+
213
+ ### The loop, in tool calls
214
+
215
+ ```python
216
+ start_emulator(avd="Pixel_7") # reuses one that is already running
217
+ install_app(build_first=True)
218
+ launch_app()
219
+ snapshot_save("clean") # ← the state every attempt returns to
220
+
221
+ run_start("issue 412: crash on empty search")
222
+ … login() / tap / type_text / expect_visible …
223
+ expect_no_crash()
224
+ run_end() # → runs/<id>/report.md
225
+
226
+ snapshot_load("clean") # next variation, from identical state
227
+ ```
228
+
229
+ [`docs/agent-guide.md`](docs/agent-guide.md) is a `CLAUDE.md` fragment you can drop into a project so
230
+ an agent picks this up without being told.
231
+
232
+ ## Try it without your own app
233
+
234
+ The repo ships **[FlakyDemo](test_app/)** — a Compose app with five screens and **27 deliberately
235
+ planted bugs**, each documented in [`test_app/BUGS.md`](test_app/BUGS.md) with what was actually
236
+ observed rather than what was intended. Crashes, races, state lost on rotation, a Save button that
237
+ reports success and silently does nothing, and several bugs that only show up one run in three.
238
+
239
+ Its flake generator is seeded, so `--el flake_seed 42` replays the same failures every time and
240
+ `--ez flake_enabled false` turns them all off for a clean baseline. It ships with 11 recipes.
241
+
242
+ It is the fastest way to see what the tool is for — and a fair test of whether it earns its keep,
243
+ since a good number of the planted bugs are the kind a tap-and-screenshot agent cannot catch at all.
244
+ The Settings screen alone reports "Saved" in the UI while the log says
245
+ `outcome=noop reason=terms_not_accepted` and nothing was written.
246
+
247
+ ## Development
248
+
249
+ ```bash
250
+ uv sync --extra dev
251
+ uv run pytest # 129 unit tests against a fake driver, no device needed
252
+ uv run ruff check src tests
253
+ ```
254
+
255
+ Setup, the live suite, and the packaging pitfalls worth knowing about are in
256
+ **[CONTRIBUTING.md](CONTRIBUTING.md)**.
257
+
258
+ ## Status
259
+
260
+ Early, but working end to end. Emulator lifecycle, snapshots, both driver backends, the screen index,
261
+ assertions, run bundles, screen recording, recipes and selector scanning are implemented and covered
262
+ by tests; the live suite passes against a Pixel 7 AVD. Packaged as a Claude Code plugin with a skill
263
+ and three commands.
264
+
265
+ Not on PyPI — the plugin builds from source, so it does not need to be. Roadmap:
266
+ [docs/roadmap.md](docs/roadmap.md). Issues and pull requests welcome.
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,240 @@
1
+ # android-driver
2
+
3
+ **Drive an Android emulator as a deterministic test harness — from Claude Code.**
4
+
5
+ [![CI](https://github.com/earlzdev/android-driver/actions/workflows/ci.yml/badge.svg)](https://github.com/earlzdev/android-driver/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
8
+
9
+ You ask an agent to reproduce a bug in your Android app. It dumps 80 KB of accessibility XML into its
10
+ own context, taps something that turns out to be the wrong element, and when the bug does not appear
11
+ it cannot repeat what it just did — because the app is now three screens deep in a state nobody
12
+ recorded.
13
+
14
+ android-driver is built for that loop instead: **build → install → drive → assert → reset → repeat.**
15
+
16
+ ```
17
+ > reproduce the crash when the bio field goes over 100 characters
18
+
19
+ snapshot_load("clean") 1.9s
20
+ open_settings() ok
21
+ type_text(id=text_field_Bio, text=110 chars) ok
22
+ expect_log("StringIndexOutOfBoundsException") ok — matched
23
+ expect_no_crash() FAILED — 1 crash record
24
+
25
+ runs/20260901-080304-repro-set-06/report.md
26
+ → java.lang.StringIndexOutOfBoundsException: begin 0, end 120, length 110
27
+ at ...screens.SettingsScreenKt.SettingsScreen$textFields(SettingsScreen.kt:149)
28
+ ```
29
+
30
+ Three attempts from the same snapshot, three identical results, and a directory of evidence to point
31
+ at. That is the whole idea.
32
+
33
+ ---
34
+
35
+ ## Install
36
+
37
+ It is a Claude Code plugin: one install brings the tools, a skill that teaches Claude the loop, and
38
+ three slash commands.
39
+
40
+ ```bash
41
+ claude plugin marketplace add earlzdev/android-driver
42
+ claude plugin install android-driver@android-driver
43
+ ```
44
+
45
+ There is no venv to manage — the plugin builds the Python server from source on demand, and passes
46
+ your project directory to it so your config is found wherever Claude was launched from.
47
+
48
+ **Requirements:** `adb` and the Android SDK's `emulator` on `PATH`, Python ≥ 3.10. For the faster
49
+ uiautomator2 backend run `python -m uiautomator2 init` once per device; without it the server falls
50
+ back to a pure-adb backend that needs nothing installed on the device.
51
+
52
+ Prefer a plain MCP server, or not using Claude Code at all? See
53
+ [docs/installation.md](docs/installation.md).
54
+
55
+ ## Quickstart
56
+
57
+ ```
58
+ /android-driver:setup
59
+ ```
60
+
61
+ It checks your toolchain, finds your `applicationId` and build command, detects whether you are on
62
+ Compose or Views, writes a starter `.android-driver.yaml`, boots an emulator and proves the loop
63
+ works. Then:
64
+
65
+ ```
66
+ /android-driver:smoke # build, install, walk the main flows, assert nothing broke
67
+ /android-driver:repro <what is broken> # reproduce it from a snapshot and leave evidence
68
+ ```
69
+
70
+ Or just talk to it — the `android-testing` skill loads automatically when a task involves driving the
71
+ app, so "check that login still works on a fresh install" does the right thing without ceremony.
72
+
73
+ ## Why it works this way
74
+
75
+ Four decisions do most of the work.
76
+
77
+ **Snapshots, so a repro is actually reproducible.** `snapshot_save` freezes the emulator's exact
78
+ state; `snapshot_load` restores it and waits until the device is genuinely drivable again — 1.8–2.2s
79
+ measured on a Pixel 7 AVD. Reinstalling and re-navigating costs 30–90s *and drifts a little each
80
+ time*. An agent testing thirty variations of a hypothesis needs the cheap, identical option: the
81
+ difference between two attempts only means something if everything else was the same.
82
+
83
+ **A screen index instead of a wall of XML.** A raw `uiautomator dump` is 50–200 KB per screen — tens
84
+ of thousands of tokens for a model that just wants to know what it can tap. `screen` returns this:
85
+
86
+ ```
87
+ device=emulator-5554 app=com.example.app/.MainActivity screen=1080x2400 driver=uiautomator2
88
+ #5 [Scroll] id=settings_container (scrollable) @(540,1236)
89
+ #7 [Text] "Settings" id=homepage_title @(235,472)
90
+ #18 [EditText] "" id=text_field_Bio @(540,1018)
91
+ ```
92
+
93
+ Then `tap(ref="#7")`. Two orders of magnitude smaller, and it reads like a menu. The raw tree is
94
+ still there behind `dump_ui_xml` for when you genuinely need it.
95
+
96
+ **Assertions that collect their own evidence.** `expect_visible` polls, so it is safe immediately
97
+ after a tap and will not flake on an animation; when it fails it hands back the screen index that
98
+ *was* there, plus a screenshot and hierarchy dump on disk. `expect_no_crash` reads the `crash` buffer
99
+ as well as `main`, because a native abort never reaches `main` at all. Wrap a sequence in
100
+ `run_start` / `run_end` and you get `runs/<id>/` holding a timeline, a report, the logcat slice for
101
+ exactly that window, and every failure artifact — so an agent cites a directory instead of describing
102
+ what it saw.
103
+
104
+ **Your flows as first-class tools.** The six steps every test starts with — sign in, create an order,
105
+ join a call — go into `.android-driver.yaml` once and become real MCP tools with typed parameters. An
106
+ agent sees `login(email, password)` in its tool list rather than rediscovering the flow from a screen
107
+ dump every session. Recipes run the same code path as the hand-driven tools, so the two cannot drift.
108
+
109
+ <details>
110
+ <summary>Plus the device knowledge that costs an afternoon each to learn</summary>
111
+
112
+ - **Uninstall-then-install**, not `pm install -r` — debug APKs from different branches carry
113
+ different signing keys and otherwise fail with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`.
114
+ - **Check `mInputShown` before pressing Back**, so dismissing the keyboard never dismisses the
115
+ dialog behind it.
116
+ - **Write to Compose `TextField`s through the accessibility node**, not tap-then-type, which lands
117
+ text in the wrong field.
118
+ - **Settle after a click** before the next query, or you read pre-animation state.
119
+ - **An `appops` pass** for OEM permission overlays that keep blocking after `pm grant` reports
120
+ success.
121
+
122
+ </details>
123
+
124
+ ## Configure
125
+
126
+ `.android-driver.yaml` at your project root is what makes a generic tool specific to your app.
127
+ `/android-driver:setup` writes a starter for you.
128
+
129
+ ```yaml
130
+ app:
131
+ package: com.example.myapp
132
+ activity: .MainActivity # optional; the launcher intent is resolved otherwise
133
+
134
+ build:
135
+ command: ./gradlew :app:assembleDebug
136
+ apk_glob: app/build/outputs/apk/debug/*.apk
137
+
138
+ driver:
139
+ backend: auto # auto | uiautomator2 | adb
140
+
141
+ selectors: # scanned, so a typo is a warning rather than a mystery
142
+ sources: ["app/src/main/**/*.kt"]
143
+
144
+ recipes: # each becomes an MCP tool with typed parameters
145
+ login:
146
+ params: {email: {required: true}, password: {required: true, secret: true}}
147
+ steps:
148
+ - launch:
149
+ - type: {desc: text_field_Email, text: "{{email}}"}
150
+ - tap: {desc: login_button}
151
+ - expect_visible: {desc: home_greeting, timeout_s: 20}
152
+ ```
153
+
154
+ The file is optional: with no config every generic tool still works — you pass `pkg=` explicitly and
155
+ lose `build_app` and recipes. It is found by walking up from your project and then, failing that, up
156
+ to three levels *down*, so an app in `app/` or `android/` is discovered without configuration.
157
+
158
+ Full reference: **[docs/configuration.md](docs/configuration.md)** · recipe and step syntax:
159
+ **[docs/recipes.md](docs/recipes.md)** · worked examples for Compose and View projects:
160
+ [`examples/`](examples/).
161
+
162
+ ## Tools
163
+
164
+ 45, plus one per configured recipe.
165
+
166
+ | Group | Tools |
167
+ |---|---|
168
+ | Emulator | `list_avds` `start_emulator` `stop_emulator` `wait_for_boot` `snapshot_save` `snapshot_load` `snapshot_list` `snapshot_delete` |
169
+ | Device | `list_devices` `select_device` `device_info` |
170
+ | App | `build_app` `install_app` `uninstall_app` `app_info` `launch_app` `force_stop` `clear_app_data` |
171
+ | UI | `screen` `tap` `tap_xy` `long_press` `type_text` `swipe` `scroll_to` `press_key` `screenshot` `dump_ui_xml` |
172
+ | Assertions | `expect_visible` `expect_gone` `expect_log` `expect_no_crash` |
173
+ | Runs | `run_start` `run_end` `run_list` `record_start` `record_stop` |
174
+ | Recipes | `list_recipes` `run_recipe` `check_recipes` `list_selectors` `reload_config` |
175
+ | Logs | `logcat_clear` `logcat_read` |
176
+ | Shell | `shell` |
177
+
178
+ > [!WARNING]
179
+ > `shell` is unrestricted on purpose — this is a development tool, not a sandbox. It can wipe device
180
+ > data, kill processes and read files. Point it at emulators and test devices, not at anything you
181
+ > care about.
182
+
183
+ ### The loop, in tool calls
184
+
185
+ ```python
186
+ start_emulator(avd="Pixel_7") # reuses one that is already running
187
+ install_app(build_first=True)
188
+ launch_app()
189
+ snapshot_save("clean") # ← the state every attempt returns to
190
+
191
+ run_start("issue 412: crash on empty search")
192
+ … login() / tap / type_text / expect_visible …
193
+ expect_no_crash()
194
+ run_end() # → runs/<id>/report.md
195
+
196
+ snapshot_load("clean") # next variation, from identical state
197
+ ```
198
+
199
+ [`docs/agent-guide.md`](docs/agent-guide.md) is a `CLAUDE.md` fragment you can drop into a project so
200
+ an agent picks this up without being told.
201
+
202
+ ## Try it without your own app
203
+
204
+ The repo ships **[FlakyDemo](test_app/)** — a Compose app with five screens and **27 deliberately
205
+ planted bugs**, each documented in [`test_app/BUGS.md`](test_app/BUGS.md) with what was actually
206
+ observed rather than what was intended. Crashes, races, state lost on rotation, a Save button that
207
+ reports success and silently does nothing, and several bugs that only show up one run in three.
208
+
209
+ Its flake generator is seeded, so `--el flake_seed 42` replays the same failures every time and
210
+ `--ez flake_enabled false` turns them all off for a clean baseline. It ships with 11 recipes.
211
+
212
+ It is the fastest way to see what the tool is for — and a fair test of whether it earns its keep,
213
+ since a good number of the planted bugs are the kind a tap-and-screenshot agent cannot catch at all.
214
+ The Settings screen alone reports "Saved" in the UI while the log says
215
+ `outcome=noop reason=terms_not_accepted` and nothing was written.
216
+
217
+ ## Development
218
+
219
+ ```bash
220
+ uv sync --extra dev
221
+ uv run pytest # 129 unit tests against a fake driver, no device needed
222
+ uv run ruff check src tests
223
+ ```
224
+
225
+ Setup, the live suite, and the packaging pitfalls worth knowing about are in
226
+ **[CONTRIBUTING.md](CONTRIBUTING.md)**.
227
+
228
+ ## Status
229
+
230
+ Early, but working end to end. Emulator lifecycle, snapshots, both driver backends, the screen index,
231
+ assertions, run bundles, screen recording, recipes and selector scanning are implemented and covered
232
+ by tests; the live suite passes against a Pixel 7 AVD. Packaged as a Claude Code plugin with a skill
233
+ and three commands.
234
+
235
+ Not on PyPI — the plugin builds from source, so it does not need to be. Roadmap:
236
+ [docs/roadmap.md](docs/roadmap.md). Issues and pull requests welcome.
237
+
238
+ ## License
239
+
240
+ MIT