--- name: navigate-app-adb description: Use to drive ApkAnalyzer's UI on a connected device/emulator via adb — dumping the screen, finding a tappable element by its visible text or content description, and tapping it — to inspect or verify a change without a human touching the device. Triggered by phrases like "navigate to X screen", "tap through the app", "check this screen on device", "verify this UI change via adb", "open app detail via adb", "go to the filter screen". --- # Skill: Navigate ApkAnalyzer via adb Drives the running app by dumping the on-screen UI tree, locating an element, and tapping its center — the same loop repeated for each hop. Use this **after** the app is installed and running (see the `run-app` skill for that part). ## The loop: dump → locate → tap → verify ### 1. Dump the current screen ```bash adb shell uiautomator dump /sdcard/window_dump.xml adb pull /sdcard/window_dump.xml . ``` (`uiautomator dump` captures every window, including Compose bottom sheets and dialogs — you don't need a separate step for those.) ### 2. Find your target node Everything on screen is locatable through two standard `AccessibilityNodeInfo` attributes that show up directly in the dump — no special app wiring required for either one: - **`text=`** — almost everything: chip labels, button text, list rows (an app row shows both its app name and package name), toolbar titles. - **`content-desc=`** — the handful of icon-only controls that render no visible text at all (every `IconButton` and the recently-viewed `Switch` — see the reference below). ```bash grep -o ']*text="Filter"[^>]*>' window_dump.xml grep -o ']*content-desc="Sort"[^>]*>' window_dump.xml ``` Tapping the bounds of a `text=` node still triggers its parent's click handler — the tap only needs to land somewhere inside the clickable ancestor. Pull the `bounds="[x1,y1][x2,y2]"` attribute off whichever node you matched. ### 3. Tap it ```bash adb shell input tap $(( (x1 + x2) / 2 )) $(( (y1 + y2) / 2 )) ``` For text fields, follow the tap with: ```bash adb shell input text "search%squery" # %s = literal space; adb input text can't take real spaces ``` To go back instead of tapping a back button node: ```bash adb shell input keyevent 4 ``` ### 4. Verify Either dump again (step 1) and confirm the expected text/description is present, or grab a screenshot for a visual check: ```bash adb exec-out screencap -p > screenshot.png ``` Then use the Read tool on `screenshot.png` to look at it directly. ## Gotchas - **On Windows, drive `adb` from PowerShell, not the Bash tool.** Git Bash rewrites `/sdcard/...` into a Windows path, so `uiautomator dump /sdcard/window_dump.xml` writes to `C:/Program Files/Git/sdcard/...` and the following `adb pull` fails. Either use the PowerShell tool or prefix the command with `MSYS_NO_PATHCONV=1`. - **Never redirect `adb exec-out screencap -p` into a file from PowerShell.** PowerShell re-encodes the stream and corrupts the PNG. Capture on the device and pull it: `adb shell screencap -p /sdcard/screen.png` then `adb pull /sdcard/screen.png`. - **Floating overlays steal taps.** A Messenger chat head or similar bubble sits above the app and swallows a tap aimed at what's underneath — which can open someone's private conversation. Tap toward the left of a row rather than its centre, and re-dump after every tap to confirm you are where you expect before tapping again. - **A locked device answers `adb` but shows nothing.** `dumpsys window | grep mDreamingLockscreen` reports `true` when the keyguard is up; a secure lock can't be dismissed from `adb`, so ask the user to unlock rather than retrying taps into a black screen. - **Off-screen elements don't dump.** A list item below the fold won't appear until you scroll (`adb shell input swipe `) or it's within the initial viewport. - **Matches aren't always unique.** Two rows can share a label (e.g. two apps with the same display name) — if a match gives more than one hit, narrow using a more specific string on the same row (an app's package name is usually unique) or the row's `bounds` to disambiguate. Localized strings also change with the device's language. - **A genuinely icon-only control needs a `contentDescription`, not a workaround.** If you add a button with no visible label, give it one via `IconButton`'s `contentDescription` parameter (a `stringResource` in the module that owns the screen — never a hardcoded literal) or, for a component that doesn't expose the parameter (e.g. `Switch`, or a `Chip` with an empty label), apply `Modifier.semantics { contentDescription = "..." }` directly at the call site. Don't reach for position-based taps — they break the moment layout shifts. Only do this for elements that render no text anywhere in their subtree; everything else is already findable by the text it already shows. ## Reference: icon-only controls (no visible text — match by `content-desc`) Everything else is findable directly by its visible text. This is the complete list of what needs `content-desc` matching instead: | `content-desc` | Element | |---|---| | `Back` | Every back button in the app — the shared `Toolbar` component, the custom app-search/permission-filter back arrows, and app-detail's collapsing toolbar. Reused everywhere since only one is ever on screen at a time. | | `Settings` | Apps list — opens settings | | `Analyze APK file` | Apps list — opens APK-file analysis | | `Sort` | Apps list — opens the sort bottom sheet | | `Delete search: ` | App search — per-row delete icon on a search-history entry | | `Clear date range` | Filter screen — the one chip that renders with an empty label | | `Recently viewed apps` | Settings — the recently-viewed-apps toggle (bare `Switch`, reuses the same string as its adjacent label) |