android-ui-automation · git:20260819.ba840b8 · 2026-08-19 · sha256 8db4aa65ab9fb38c

android-ui-automation git:20260819.ba840b8A

Immutable. This exact content is served forever at /api/v1/blob/8db4aa65ab9fb38c.

---
name: android-ui-automation
description: >
  Automate Android apps that have no public API or web version by driving the
  UI layer through the Accessibility Service. Handles: deep-link launch (open
  an app into a specific screen), intelligent node location (find a button or
  text by fuzzy match, not exact text), tap-and-verify (confirm an action had
  its effect), and human-like swipes. Use this whenever the user wants to
  control a phone-only app on Android — e.g. "open Spotify and play X",
  "tap the like button", "swipe through my feed", "play 连名带姓", anything
  involving `spotify://`, `weixin://`, or apps with no web/API surface. Also
  use when the user asks why an automation didn't work (permissions, missing
  app, hidden view trees). Android-only; requires the Minis Accessibility
  Service enabled.
compatibility: Android-only; requires the Minis app (Android) with its bundled android-a11y-cli / android-open CLIs and the Minis Accessibility Service enabled
---

# Android UI Automation

Drive Android apps through their **UI layer** when they have no public API or
web version. This is the fallback that makes "phone-only" apps automatable.

## Environment & dependencies (read first)

This skill drives **the user's Android phone**. It relies on two CLI tools
that ship with the **Minis app for Android** (installed at
`/usr/local/bin/` inside the Minis runtime):

| CLI | Role |
|---|---|
| `android-a11y-cli` | Accessibility-Service bridge: `ui dump`, `tap node`, `gesture swipe`, `wait`, `extract`, ... |
| `android-open` | Launch any URL/scheme via the system handler (`spotify://...`, `weixin://...`, `tel:`, ...) |

**Prerequisites on the device:**

1. Minis app installed (Android). These CLIs do **not** exist on other hosts
   (macOS/Windows/other Linux) — verify with `which android-a11y-cli` first,
   and if missing, tell the user this skill only runs inside Minis on Android.
2. **Minis Accessibility Service enabled**: Settings → Accessibility → Minis.
   Check with `android-a11y-cli service ping`; if it reports not running, ask
   the user to re-enable it before anything else.
3. The target app installed on the same device.

These are **device-side capabilities**, not network APIs — no API keys, no
OAuth, no server. If a step returns `permission_denied` from the a11y service,
it means the service was revoked; recover via Settings → Accessibility.

## When to use

- The target app has **no API / no web page** (WeChat Moments, many CN apps).
- Offload fine-grained clicking to reliable scripts instead of brittle commands.
- The user asks to **play/search/open/tap/swipe** inside an installed Android app.

Do **not** use this for apps that already have a proper skill (e.g.
`bilibili-hub`, `spotify-hub`) — prefer the API path when one exists.

## Core workflow

Follow this order. Each step has a reason; don't skip verification.

1. **Resolve the target**
   Confirm the app is installed. If unsure, ask the user or try the deep link
   and check the foreground package (see step 2).

2. **Launch via deep link** — `scripts/open_deep_link.py`
   ```
   python3 scripts/open_deep_link.py "spotify://search/circles%20post%20malone" --pkg com.spotify.music
   ```
   - It constructs/opens the link **and verifies the app reached the foreground**.
   - Percent-encode query values (CJK and `&`/`?` break raw links).
   - If it returns `ok:false`, don't proceed — the app is missing, the scheme
     is unhandled, or something redirected. Surface the error.
   - ⚠️ Some apps (Spotify) land on a **suggestion page**, not results — tap a
     suggestion row first (see `references/app-deep-links.md`), then locate the
     result.

3. **Locate the node** — `scripts/find_node.py`
   ```
   python3 scripts/find_node.py "Circles" --clickable-only --top 5
   ```
   - Uses fuzzy scoring (exact > substring > node-fragment), prefers clickable.
   - Read the `nodeId` and `center` from the top result.

4. **Tap and verify** — `scripts/tap_and_verify.py`
   ```
   python3 scripts/tap_and_verify.py --query "Circles – Post Malone" --marker "暂停"
   ```
   - The `marker` is the proof it worked (e.g. after tapping play, a **pause**
     button must appear). Never report success without a marker.
   - Retries a few times with relocation in case the tree changed.

5. **Human-like swipes** (when the tree is unavailable) — use gestures:
   ```
   android-a11y-cli gesture swipe X1 Y1 X2 Y2
   ```
   Vary distance/pause randomly to look natural. This is the ONLY fallback when
   an app (like WeChat Moments) hides its view tree.

## Why the verify step matters

A raw tap is fire-and-forget: the UI may be mid-animation or the node may have
drifted. Verifying a **success marker** (pause button, title, toast) converts
"I clicked something" into "I know it worked". If the marker never appears,
say so honestly instead of claiming success.

## Known limitations

- Apps can **hide their view tree** from the Accessibility Service (WeChat
  Moments is a known case). When `ui dump` returns no app nodes, **do NOT
  blind-tap guessed coordinates** — either use swipes (scroll-only) or ask the
  user to tap precisely.
- The Accessibility Service can be **revoked** (e.g. after a force-stop). Check
  `android-a11y-cli service status`; if not running, ask the user to re-enable
  it in Settings → Accessibility.
- Deep-link support varies by app/build. Read `references/app-deep-links.md`
  for the tested set and always confirm foreground.

## Examples

**Play a song on Spotify (no API key, no Premium):**
```
python3 scripts/open_deep_link.py "spotify://search/circles%20post%20malone"
python3 scripts/find_node.py "添加建议"          # suggestion page → tap to get results
python3 scripts/tap_and_verify.py --query "添加建议“circles post malone”" --marker "Circles"
python3 scripts/find_node.py "Circles – Post Malone" --clickable-only
python3 scripts/tap_and_verify.py --query "Circles – Post Malone" --marker "暂停"
```

**Human-like feed scroll (WeChat Moments):**
```
android-a11y-cli ui info            # confirm weixin:// foreground
android-a11y-cli gesture swipe 540 1600 540 800   # repeat with varied distance/pause
```

**Decline gracefully when the app can't be reached:**
Report `ok:false` from `open_deep_link.py` rather than proceeding blind.

## Output format

Always report the confirmed state, e.g.:

# [Result]
- Opened: spotify://search/... via deep link, foreground=com.spotify.music
- Tapped: node <id> ("Circles – Post Malone")
- Verified: playback bar shows pause button → playing