Reverse-map a competitor Android app's screens and navigation into a flow graph
plus a clean, English UX report — by driving a physical device over adb only,
with zero device-side footprint (no instrumentation APK, no agent on the device).
Built for competitive UX research. Works on normal native apps, WebView PWAs, and locked-down POS/payment terminals. Language-agnostic: it maps English and non-Latin (e.g. Persian/Arabic) UIs, and its output is always in English.
It ships as a Claude Code skill (SKILL.md) plus
standalone Python scripts you can run directly.
An instrumentation APK on a payment terminal can trip attestation or show up in a
vendor audit. Everything here goes through adb shell — uiautomator dump for the
UI tree, screencap for pixels, input tap for actions. Slower than a helper APK,
but leaves nothing on the device.
A portable report/ folder (English throughout):
report/
├── screens.md # inventory: #, name, purpose, entry path (taps from home), element count, screenshot
├── flows.md # per-section ordered tap paths, home → screen
├── walls.md # blocked / gated / needs-human / non-ASCII-input items — the manual-pass worklist
├── comparison.md # competitor taps-to-reach filled; your app's column left for a human
├── graph.mmd # Mermaid flowchart — paste into any Mermaid viewer
└── screenshots/ # every screen copied + renamed NN-<name>.png (English)
Plus the machine-readable run/<timestamp>/graph.json (screens, edges, flags).
- Never autonomously taps money-moving or state-changing controls. A regex blocklist
(
BLOCKED_PATTERN, English + Persian out of the box) gates every tap; blocked controls are recorded as walls, never pressed. Adding terms is encouraged; removing one is a deliberate, recorded decision. - No device-side installs without a human's say-so.
- Captured screenshots / APKs never leave the machine — no online analyzers, no cloud
device farms, and no LLM API. Screens carry account/merchant/transaction data. The
repo's
.gitignoreexcludesrun/,report/, and all*.png/*.apkso you can't publish them by accident. - Deterministic by default. Phases 0–2 use no model at all. An optional model-driven policy (Phase 3) exists in the design but is gated — it would send screenshots to an API, so it must not run without explicit sign-off.
- Deterministic Python always owns the graph, frontier, dedup, replay, budget, and blocklist. A model, if ever added, only chooses which element to touch.
As a Claude Code skill:
git clone https://github.com/IFDS-OSS/reverse-competitors ~/.claude/skills/reverse-competitorsThen invoke /reverse-competitors in Claude Code, or just run the scripts directly.
# 0. one device attached
adb devices
# target
export POS_SERIAL=<serial> # auto-detects if exactly one device
export POS_PKG=<package> # required
# 1. probe the device + find the package
python scripts/preflight.py --find <app-substring>
# 2. (interactive) sanity-check the bridge by hand; characterize BACK
python scripts/drive.py --app "$POS_PKG"
# 3. deterministic crawl (no model)
python scripts/crawl.py --budget 300 --max-depth 6 --time-limit 45
# 4. build the report from the newest run
python scripts/report.pyLog in / clear any OTP gate by hand first — the crawl runs from an authenticated screen.
- WebView PWA apps — if the SPA won't render, an old device WebView may be failing to
parse a modern JS bundle (
logcatshowsSyntaxError); updating the WebView is a device-side install → ask first. Once it renders, the WebView a11y bridge exposes the DOM touiautomator. - Coordinate frontier — div-soup SPAs mark nothing clickable, so the frontier is built
from text /
content-descnodes and tapped by coordinate. Labels are English-first (ASCIIcontent-desc/resource-idpreferred over on-screen text). - Stable fingerprints — the screen fingerprint collapses repeated sibling subtrees, so a list of 3 vs 30 rows is one screen, not many. Dedup falls back to resource-id then label-set Jaccard (WebViews often lack resource-ids).
- Backtracking — hardware BACK is characterized first and usually avoided (on some POS
ROMs it exits the app and hangs
adb); the crawler backtracks by restart + replaying the shortest path from home. - Resilience — adb stalls are recoverable, a flaky action is skipped not fatal, and
graph.jsonis always written on exit.
| file | role |
|---|---|
bridge.py |
device layer: adb transport, UI parse, fingerprint, blocklist |
preflight.py |
Phase 0 — probe device, latency, package |
drive.py |
Phase 1 — interactive manual driver + session recorder |
stepwalk.py |
supervised one-action-at-a-time stepper + coordinate frontier |
crawl.py |
Phase 2 — deterministic BFS crawler |
report.py |
Phase 4 — render report/ from graph.json |
See SKILL.md for the full method, phase gates, and acceptance criteria.
MIT — see LICENSE.