← LimrunCONTENT HISTORY

Update to Limrun

Snapshot Sep 30, 2026 · 23:08 UTC · version 1.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "limrun-android-emulator",
  "description": "Drive an app running on a Limrun cloud Android emulator: install an APK, launch and terminate apps with crash reports, tap, type, read the UI element tree, screenshot, record video, inject microphone audio, shape network bandwidth, read app logs, run shell commands, transfer files, tunnel the app's network destinations through your machine with HTTP inspection and HAR capture, and use adb over the CLI's tunnel for full logcat and interactive tools. Use after a build (from limrun-gradle or any builder) when the user wants to see, test, or interact with their app on an emulator, or says 'show me a screenshot', 'tap', 'run it on the emulator', 'check logcat', 'record a video', 'inspect network traffic', or 'reach my local server from the emulator'. To build the APK or AAB first, use limrun-gradle.",
  "included_files": [],
  "skill_md_contents": "---\nname: limrun-android-emulator\ndescription: \"Drive an app running on a Limrun cloud Android emulator: install an APK, launch and terminate apps with crash reports, tap, type, read the UI element tree, screenshot, record video, inject microphone audio, shape network bandwidth, read app logs, run shell commands, transfer files, tunnel the app's network destinations through your machine with HTTP inspection and HAR capture, and use adb over the CLI's tunnel for full logcat and interactive tools. Use after a build (from limrun-gradle or any builder) when the user wants to see, test, or interact with their app on an emulator, or says 'show me a screenshot', 'tap', 'run it on the emulator', 'check logcat', 'record a video', 'inspect network traffic', or 'reach my local server from the emulator'. To build the APK or AAB first, use limrun-gradle.\"\nuser-invocable: true\neffort: high\n---\n\n# Limrun Android Emulator\n\nInteract with an app running on a Limrun cloud Android emulator, from any\nenvironment (Linux, Windows, macOS, VM, container). This skill is\nbuild-agnostic: it assumes the APK was already built, usually by\n**limrun-gradle**. Keep build concerns in that skill; this one is about\ndriving the running emulator.\n\nNever use a local emulator, a local Android SDK, or Android Studio.\n\n## Auth and CLI\n\nInstall if needed: `npm install --global lim`. Auth is `lim login` or\n`LIM_API_KEY` (it may be set outside the project, so don't ask for it just\nbecause it's missing from `.env` or the shell). The CLI is the source of truth:\nthe commands in this skill are verified, but if a flag errors or you need one\nnot shown here, check `lim android <subcommand> --help` instead of guessing.\n\n## Installing an app\n\nYou can build with Limrun's remote Gradle service and have the APK installed\non a fresh emulator automatically, or install a pre-built local APK or a URL.\nOne default to know first: `lim android create` opens an ADB tunnel\n(`--connect`) and a browser tab with the live stream (`--open`) unless told\notherwise. As an agent, pass `--no-open` always, and `--no-connect` unless you\nneed adb right away.\n\n### Build and install\n\nUpload the APK built by **limrun-gradle** as a named asset, then create an\nemulator with it pre-installed:\n\n```bash\nlim gradle build . --upload myapp.apk\nlim android create --install-asset myapp.apk --no-open --no-connect\n```\n\nCreate blocks until the instance is ready to drive; no boot wait is needed.\nThe create output includes a signed stream URL; share it with the user as a\nMarkdown link, like `[Live emulator](<signed-stream-url>)`. If you have a\nbrowser the user can see, open the URL there and tell them. Create also\nprints a console URL: it opens the same live view but requires a console\nlogin, so prefer the signed stream URL for sharing.\n\nUseful create flags: `--reuse-if-exists` (reuse a running instance with the\nsame labels), `--rm` (delete when the CLI exits), `--jurisdiction us|eu|as`\n(where the instance runs; don't use `--region`, it is deprecated),\n`--inactivity-timeout` / `--hard-timeout`, `--display-name`, and `--label k=v`\n(find labeled instances later with `lim android list --label-selector k=v`).\n\n### Install app from local\n\nCreate a new emulator, then install a local file or a URL:\n\n```bash\nlim android create --no-open --no-connect\nlim android install-app ./app-debug.apk\nlim android install-app https://example.com/app.apk\n```\n\nA local path is uploaded to Limrun Asset Storage first; a URL is fetched by\nthe instance itself, which is much faster for large APKs than uploading from\nyour machine. `install-app` returns as soon as the app is sent; the install\nfinishes in the background within seconds. Newly installed apps land in the\napp drawer, not the home screen, so don't look for their icon; just launch\nthe app with `lim android launch-app <package> --detach` (without `--detach`\nit blocks watching the app until it exits), or confirm with\n`lim android adb-shell -- sh -c \"pm list packages | grep <name>\"`.\n\nEvery time you need to install a new version of the APK, sync it instead of\nreinstalling:\n\n```bash\nlim android sync ./app-debug.apk\n```\n\nIt sends only a delta against the APK already on the instance, then\nreinstalls. `--watch` keeps re-syncing on file changes, and\n`--launch-mode ForegroundIfRunning|RelaunchIfRunning` controls what happens to\nthe running app after each install.\n\n## Targeting the right instance\n\nMost `lim android` commands default to the last created instance and resolve\nthe \"current\" one from the **git repo / worktree** of your cwd. In a different\ndirectory (or outside any git repo) a command can report that no recent\ninstance was found even though one is running. The reliable recipe:\n\n```bash\nlim android list                          # shows all instances and their IDs\nlim android element-tree --id <that-id>   # pass --id to EVERY lim android command\n```\n\nOnce you have the ID (format `android_<region>_<ulid>`), pass\n`--id <android-instance-id>` to all `lim android` calls for the rest of the\nsession. Alternatively, `git init` the project so the workspace resolves on\nits own. When controlling multiple instances, always pass `--id`.\n\n## Launching the app\n\nLaunch and stop installed apps by package name:\n\n```bash\nlim android launch-app com.example.app --detach                  # launch and return\nlim android launch-app com.example.app                           # launch and watch until it exits\nlim android launch-app com.example.app --mode RelaunchIfRunning  # restart for a clean state\nlim android terminate-app com.example.app                        # stop it, e.g. to reset app state\n```\n\nWithout `--detach`, `launch-app` blocks watching the app: when it crashes,\nANRs, or is stopped, the command prints the exit reason, crash details with\nthe stack trace, and a recent app log tail, then returns. That report is the\nway to see why an app died without adb; for logs while the app is running,\nuse `lim android app-log` (below). There is no `list-apps`; discover package\nnames with `lim android adb-shell -- pm list packages`, or take the\napplication ID from the build.\n\n## App logs\n\nOne app's logs need no tunnel:\n\n```bash\nlim android app-log com.example.app --tail 100   # recent lines (app must be running)\nlim android app-log com.example.app --follow     # stream live lines until Ctrl+C; don't stream into context\n```\n\n## Shell and files\n\nOne-shot shell commands and file transfer need no tunnel either:\n\n```bash\nlim android adb-shell -- pm list packages -3                     # like adb shell; args go after --\nlim android adb-shell -- sh -c \"dumpsys battery | grep level\"    # pipes need an explicit shell\nlim android push-file ./fixture.json /sdcard/Download/fixture.json\nlim android pull-file /sdcard/Download/out.json ./out.json\n```\n\nThey run with the same permissions the adb shell user has, and `adb-shell`\nexits with the command's exit code.\n\n## Full logcat and interactive adb over the tunnel\n\nFull-device logcat and anything interactive (Android Studio, scrcpy,\nstreaming) go through plain `adb` over the CLI's tunnel. Start the tunnel in\na background shell and keep it alive:\n\n```bash\nlim android connect        # prints \"Tunnel started on 127.0.0.1:<port>.\"\n```\n\n`connect` runs `adb connect` for you (use `--adb-path` if adb isn't on PATH).\nThe printed `127.0.0.1:<port>` is the device serial; pass it with `-s` to\nevery adb call (`adb devices` also lists it):\n\n```bash\nSERIAL=127.0.0.1:<port>\nadb -s $SERIAL logcat -d | tail -100    # dump recent full-device logs, don't stream into context\n```\n\nThe tunnel lives and dies with the process that started it: when that shell\nexits, `adb devices` shows the serial as `offline` while the instance keeps\nrunning. Just run `lim android connect` again to get a new tunnel (the port\nchanges each time). Stale offline serials are harmless; `adb disconnect`\nclears them.\n\n## Testing changes\n\nWhen emulator interaction is part of the task, test new or changed\nfunctionality with the interaction commands after each install or sync. Focus\non what changed, plus a quick smoke test of core flows. Start by reading the\nelement tree to see what's on screen before acting:\n\n```bash\nlim android element-tree\n```\n\nThe output is the raw UIAutomator XML hierarchy on a single line, so a plain\ngrep echoes the whole document. Split it into one node per line first, then\ngrep for the `text`, `resource-id`, `content-desc`, or `bounds` you need\nrather than dumping the whole tree into context:\n\n```bash\nlim android element-tree | sed 's/></>\\n</g' | grep -i \"save\"\n```\n\n## Interacting with the app\n\nPrefer tapping by resource id, then by visible text or content description,\nthen coordinates as a last resort:\n\n```bash\nlim android tap-element --resource-id com.example.app:id/startButton\nlim android tap-element --text \"Save\"\nlim android tap-element --content-desc \"Open menu\"\nlim android tap 360 800\n```\n\nSelector values match **exactly**, not by substring: `--text \"Save\"` does not\nmatch a \"Save draft\" button. Copy the value verbatim from `element-tree`. A\nselector that matches nothing fails in a couple of seconds with\n`No element found for selector`. Other selectors: `--class-name`,\n`--package-name`, `--index`, `--clickable`, `--enabled`, `--focused`, and\n`--bounds-contains-x/y`; combine them to narrow a match. On web pages in the\nbrowser, resource ids are the page's own DOM ids (like `searchIcon`) and are\noften empty; select by `--text` plus `--class-name` there, or fall back to\ncoordinates from the node's `bounds`. To inspect matches without tapping, use\n`find-element` with the same selectors:\n\n```bash\nlim android find-element --text \"Save\"   # table of matches with bounds\n```\n\nFor text input, target the field directly; no prior focus tap is needed.\n`type` takes the same selectors as `tap-element` (`--class-name\nandroid.widget.EditText --focused` works for a field with no resource id):\n\n```bash\nlim android type \"hello world\" --resource-id com.example.app:id/searchBox\nlim android type \"hello\" --x 360 --y 400   # by coordinate\nlim android press-key enter\nlim android press-key backspace            # --modifier shift/control/alt/command to combine\n```\n\nFor scrolling and navigation:\n\n```bash\nlim android scroll down --amount 600\nlim android scroll up --amount 300\nlim android press-key back\nlim android press-key home\nlim android open-url \"https://example.com\"   # opens in the default browser; also fires deep links\n```\n\nAfter every interaction, re-run `element-tree` to confirm the UI transitioned.\nNo sleep is needed between a tap and `element-tree`; the tap blocks until\ndone. A page load after `open-url` is asynchronous though: re-run\n`element-tree` until the node you expect appears. A present but childless\n`android.webkit.WebView` means the page is still loading, not a broken tree.\n\n```bash\nlim android element-tree\n```\n\n### When the element tree is empty\n\nSome React Native and Expo apps expose no accessibility nodes at all, which\nleaves `element-tree`, `tap-element`, and `find-element` blind (system dialogs\nstill expose nodes). Fall back to driving by pixels: take a screenshot, read\nthe coordinates of the target, and use `tap x y` / `type --x --y`. Screenshot\npixels map 1:1 to tap coordinates, so a button centered at (360, 1322) in the\nimage is tapped with `lim android tap 360 1322`. Re-screenshot after each\naction to confirm the result.\n\n## Screenshots and video\n\nScreenshot takes a **positional path** (not `-o`):\n\n```bash\nlim android screenshot screenshot.png\nlim android screenshot screenshot.png --id <android-instance-id>\n```\n\nUse the element tree for functional assertions (element existence, text, state\nchanges) and screenshots only for visual properties. For anything involving\nmotion (animations, gameplay, streaming UI), prefer video:\n\n```bash\nlim android record start                     # non-blocking\nlim android record stop -o /tmp/recording.mp4\n```\n\n`record stop` accepts `--quality 5-10`. Recorded frames are half the\nscreenshot resolution, so read tap coordinates from screenshots, never from\nvideo frames. For UI changes, include a demo video in the pull request so the\nuser can see it.\n\n## Simulate the microphone with an audio file\n\nFor voice-driven flows (assistants, speech-to-text, audio calls), play a local\naudio file as the emulator's microphone. The app hears the audio through its\nnormal capture pipeline:\n\n```bash\nlim android play-on-microphone ./fixtures/command.wav          # loops by default\nlim android play-on-microphone ./fixtures/command.mp3 --once\n```\n\nWAV and MP3 work. The file is pushed over adb, so this command needs a local\n`adb` binary (`--adb-path` if it's not on PATH) and opens its own short-lived\ntunnel. Camera injection is iOS-only; for camera-driven test flows use\n**limrun-ios-simulator**.\n\n## Shape network bandwidth\n\nTest slow-network behavior by capping the instance's Wi-Fi bandwidth:\n\n```bash\nlim android set-wifi-bandwidth --down-kbps 1000 --up-kbps 500\nlim android set-wifi-bandwidth --down-kbps 0 --up-kbps 0       # 0 clears the limit\n```\n\n## Tunnel the app's traffic through your machine\n\nWhen the app must reach a service only your machine can reach (a local dev\nserver, a VPN-only staging API), or you need to see its HTTP traffic, start a\ndestination tunnel. Only the destinations you select are rerouted through\nthe machine running `lim`; everything else leaves the instance directly.\n\n```bash\nlim android tunnel --selector localhost:8080 --detach --id <android-instance-id>\n```\n\n- An exact selector (`localhost:port` or `IP:port`, port >= 1024) becomes a\n  listener on the emulator, also reachable as `10.0.2.2:<port>`; the app's\n  connections to it land on your machine and are dialed there.\n- Domain selectors (`api.example.com`, `\"*.corp.example\"`) are intercepted\n  on the emulator and dialed from your machine, so your DNS and VPN apply.\n  Apps that resolve DNS themselves over HTTPS bypass domain interception.\n- Start the tunnel **before** launching the app: connections opened earlier\n  keep their original route. One tunnel per instance; a second start fails.\n\nAs an agent, always pass `--detach`: it returns once the tunnel is READY and\nkeeps it alive in a background process. Manage it with:\n\n```bash\nlim android tunnel status --id <android-instance-id>   # state, per-selector binds, last dial failure\nlim android tunnel stop --id <android-instance-id>\n```\n\n### Inspect HTTP traffic, capture HAR, persist a network log\n\nInspection is on by default: every HTTP and HTTPS request through the tunnel\nis decoded, printed as one summary line per request (in the tunnel log file\nwhen detached), and shown live in the console's network panel.\n\n```bash\nlim android tunnel --selector \"*.api.example\" --har ./traffic.har --detach   # write HAR 1.2 with bodies\nlim android tunnel --selector \"*.api.example\" --persist --detach             # network log survives the instance\n```\n\n`--persist` uploads a body-inclusive network log as a session artifact when\nthe tunnel stops or the instance terminates; it appears on the instance's\nsession page in the console with a HAR download (default lifetime 3 days,\n`--ttl <seconds>` up to 30 days). HTTPS is decoded with an emulator-trusted\nCA, so **apps with certificate pinning fail through inspected domain\nselectors**: leave the pinned host out of the selectors or pass\n`--no-inspect` to relay bytes opaquely (no summaries, HAR, or persistence).\n\n## Preview URL for humans\n\nUpload the APK to Limrun Asset Storage and return a preview URL for the user\nto open and test the app manually in the browser:\n\n```bash\nexport ASSET_NAME=myapp.apk   # can be any name\nlim asset push ./app-debug.apk -n ${ASSET_NAME}\n\necho \"https://console.limrun.com/preview?asset=${ASSET_NAME}&platform=android\"\n```\n\nOpening the link in the Limrun console provisions an emulator with the APK\npre-installed.\n\n## Cleanup\n\nWhen the work is completed, you can delete the emulator. `delete` takes a\n**positional** ID (`--id` is not a valid flag here, unlike other commands):\n\n```bash\nlim android delete <android-instance-id>\n```\n\n## Gotchas\n\n- **The fleet is x86_64.** Emulators report `x86_64,arm64-v8a` ABIs and run\n  arm64 code through translation, but an APK whose native libraries are\n  arm64-only for some vendor SDKs installs fine and then crashes with\n  `UnsatisfiedLinkError` when that code first loads. Build with x86_64 native\n  libs included.\n- **Selectors match exactly.** `tap-element --text` and `find-element --text`\n  need the full, exact string from `element-tree`; substrings match nothing.\n- **`install-app` returns before the install finishes.** The app lands a few\n  seconds later; verify with `find-element` or `pm list packages` before\n  launching. Prefer `install-app <URL>` or `create --install-asset` over raw\n  `adb install`: a big APK over `adb install` streams with zero progress\n  output and looks hung for minutes, and killing it mid-stream corrupts the\n  install.\n- **The ADB tunnel is session-bound.** It dies with the shell that started it\n  while the instance keeps running; reconnect with `lim android connect` and\n  re-read the port, it changes every time.\n- **Failed create pipes can still leak an instance.** If a `create` invocation\n  errors client-side (broken pipe, JSON parse), check `lim android list`; the\n  instance may exist anyway and should be deleted.\n- **Empty element tree usually means a React Native app**, not a broken\n  instance. See \"When the element tree is empty\" above.\n- **`element-tree` can be large.** Pipe through `grep` to extract what you\n  need rather than dumping the whole tree into context.\n- **Instance resolution can miss in a non-git dir.** See \"Targeting the right\n  instance\" above; pass `--id` when in doubt.\n- **Build errors are the build skill's job.** If the APK isn't building, the\n  failure is upstream; go back to **limrun-gradle**.\n"
}

SHA-256: a535aa9b5148618e59c6cf840a3755bf05b1916aadda3c8b19cf5566b04ebf09