# Debugging an Android app crash with a coding agent on a cloud emulator

A coding agent took a one-line Android bug report to a tested fix in under 10 minutes on a cloud emulator. The commands, timings, stack trace, and mistakes.

- Canonical: https://replicas.dev/resources/debug-android-crash-with-a-coding-agent

This is the bug report we gave Claude Code:

> **App closes when I open Ask**
>
> Since updating to 1.4 the app closes as soon as I tap the Ask tab. Top and Show work fine. Pixel 8, Android 15. Reinstalling didn't help.

9 minutes and 47 seconds later, it had reproduced the crash on a cloud Android emulator, found the line, fixed it, written a test that fails without the fix, and checked the fixed build on the same emulator. Almost 4 of those minutes went to a recording command that never worked.

We wrote the app and planted the bug for this test. The agent got the report and the repo, and nothing else.

![Side by side on an Android 15 emulator: version 1.4 drops to the home screen when Ask is tapped, while the fixed build lists Ask HN posts](https://api.replicas.dev/v1/media/public/4a2a4c8c-c2c6-49d8-b60c-c8e2c562eeb7/mRWmfjEnxp4nzOESr7wrTq5pCSNs78DelVv4uTnuhOM)

The agent's own recordings of both runs: [version 1.4](https://api.replicas.dev/v1/media/public/1f63d22a-d310-4ffb-a2bd-47913a872d37/JEF9p-LbblCRu-5qO-PBUNKFKY2t7lDxB_B0drKZnD8) and [with the fix](https://api.replicas.dev/v1/media/public/26c578fc-4717-4333-a1fa-726e15ed2403/wBuPCxjILY8m6TsJLGGtZHF-YGR8Dsw7ObxxOLnuz4w).

## The app and the setup

HN Reader is a 184-line Kotlin app for the [Hacker News API](https://github.com/HackerNews/API), with Top, Ask, and Show tabs. It uses Retrofit and Gson for network calls and a RecyclerView for the list. Version 1.4 added two things: each story shows its site, like `github.com`, under the title, and tapping a story opens the article instead of the comments.

The agent was Claude Code on Opus 5.5, in a Replicas workspace: Linux, 8 vCPUs, and no `/dev/kvm`, so no local emulator. The [mobile testing guide](/resources/test-mobile-apps-with-cloud-coding-agents) explains why most cloud sandboxes can't run one. Its prompt was the bug report plus:

> Reproduce it on a cloud Android emulator (the `lim` CLI is already set up in this workspace; the limrun skills explain how to use it), find the cause, fix it, and confirm the fix on the emulator. Add a test or a repeatable repro so this doesn't come back. Commit the fix to the repo.

We had installed JDK 17 and the Android SDK command-line tools beforehand, which took 18 seconds, and built the app while writing it, so Gradle's dependency cache was warm. Every time below comes from the agent's transcript, counted from when it got the prompt.

![Timeline of the run: build at 0:28, emulator at 0:56, 3 minutes 52 seconds lost to recording timeouts, crash reproduced at 5:20, fix written at 6:25, failing test at 6:55, passing test at 7:25, verified on the emulator by 9:12, and the emulator ran for 8 minutes 34 seconds](/blog/debug-android-crash-with-a-coding-agent-timeline.png)

## The build (0:18)

The Limrun Gradle skill tells agents to build with `lim gradle build`, which runs Gradle on a remote sandbox. The agent tried that first:

```
$ lim gradle build . --upload hn-reader-1.4.apk
    Error: 403 {"error":"Unsupported mobile testing operation"}
```

Replicas gives each workspace scoped access to [simulators, emulators, and Xcode sandboxes](https://docs.replicas.dev/features/mobile-testing), but not to Limrun's Gradle sandboxes. Building an APK doesn't need KVM, though, and the SDK was installed, so the agent ran `./gradlew assembleDebug` on the workspace. It took 24 seconds.

## An emulator, then a dead end (0:56)

```bash
lim android create --no-open --no-connect --display-name hn-reader-ask-crash \
  --label task=hn-ask-crash --inactivity-timeout 45m
lim android install-app /tmp/hnapk/hn-1.4-broken.apk --id $ID
lim android adb-shell --id $ID -- getprop ro.build.version.release
```

`lim android create` returned in about a second (the CLI reported 0.6 seconds), the 6.4 MB APK was sent in 3.7 seconds, and the emulator reported Android 15, the version in the bug report.

Next, the agent tried to start a screen recording for its write-up. `lim android record start` failed after 30 seconds with `Error: Request startRecordingSession timed out`. It tried again, tried once without the CLI's background daemon, and ran `record stop` once, which hung for two minutes until the agent killed it. Recording requests go from the CLI straight to the emulator's host, not through Replicas. After the run, the same command timed out twice more on a new emulator, so this looks like a problem on Limrun's side.

After 3 minutes and 52 seconds, the agent switched to Android's own `screenrecord`, which runs on the device and needs no adb tunnel. It started a 40-second recording in the background, reproduced the bug, and then pulled the file:

```bash
lim android adb-shell --id $ID -- sh -c \
  "(screenrecord --time-limit 40 --bit-rate 4000000 /sdcard/before.mp4 >/dev/null 2>&1 &)"
lim android pull-file /sdcard/before.mp4 ~/hn-reader-media/before-crash.mp4 --id $ID
```

## The crash (5:20)

Without `--detach`, `lim android launch-app` [watches the app](https://github.com/limrun-inc/skills/blob/main/skills/limrun-android-emulator/SKILL.md) until it exits, then prints why, with the stack trace and the app's recent logs. The agent started it in the background, waited for the Top list to load, and tapped Ask by its label:

```bash
lim android launch-app dev.replicas.hnreader --id $ID > /tmp/hnapk/launch-before.txt 2>&1 &
lim android tap-element --text Ask --id $ID
```

In the recording, the app shows a loading spinner and is gone about a second after the tap. The watcher's output, trimmed:

```
dev.replicas.hnreader exited (reason: crash)
Crash in dev.replicas.hnreader (pid 5741): java.lang.NullPointerException
java.lang.NullPointerException: uriString
	at android.net.Uri$StringUri.<init>(Uri.java:505)
	at android.net.Uri$StringUri.<init>(Unknown Source:0)
	at android.net.Uri.parse(Uri.java:467)
	at dev.replicas.hnreader.StoryAdapter.onBindViewHolder(StoryAdapter.kt:23)
	at dev.replicas.hnreader.StoryAdapter.onBindViewHolder(StoryAdapter.kt:12)
	at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7747)
	...
	at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:687)
--- Recent app logs (116 lines) ---
```

None of this needed an adb tunnel.

## Why a non-null String was null (5:28)

Line 23 of `StoryAdapter.kt` was new in 1.4:

```kotlin
holder.binding.site.text = Uri.parse(story.url).host?.removePrefix("www.")
```

The same release added `val url: String` to the `Story` model. Kotlin says that can't be null. Three things made it null anyway:

- **The API leaves `url` out of text posts.** The Hacker News API marks only `id` as required, and its [example Ask HN item](https://github.com/HackerNews/API#items) has `text` and no `url`. The agent checked the live feeds with `curl`: 29 of the first 30 Ask items had no `url`, and all 30 Top items had one. That's why Top and Show worked.
- **Gson skips Kotlin's null checks.** When a class has no no-argument constructor, Gson [creates it through `Unsafe`](https://github.com/google/gson/blob/main/Troubleshooting.md) without running the constructor, so the missing field stays null. Gson's [README](https://github.com/google/gson#readme) says Kotlin's non-null types "are not supported" and that this "can lead to confusing and incorrect behavior."
- **The null travels until something uses it.** `Uri.parse` [throws `NullPointerException("uriString")`](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/net/Uri.java) on null input, which is `Uri.java:505` in the trace. Kotlin's docs list "issues caused by external Java code" among the [few ways Kotlin code can still hit a NullPointerException](https://kotlinlang.org/docs/null-safety.html).

The agent read the 1.4 commit with `git show HEAD` and changed the click handler too, since the handler passed the same `story.url` to `Uri.parse`. Fixing only line 23 would have moved the crash from opening Ask to tapping an Ask post.

## The fix (6:25)

```diff
-    val url: String,
+    val url: String?,
```

```diff
+        val commentsUri = Uri.parse("https://news.ycombinator.com/item?id=${story.id}")
+        val articleUri = story.url?.let(Uri::parse)
         holder.binding.title.text = story.title
-        holder.binding.site.text = Uri.parse(story.url).host?.removePrefix("www.")
+        holder.binding.site.text = articleUri?.host?.removePrefix("www.")
+        holder.binding.site.isVisible = articleUri != null
         holder.binding.meta.text = context.getString(R.string.story_meta, story.score, story.by, story.descendants)
         holder.itemView.setOnClickListener {
-            context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(story.url)))
+            context.startActivity(Intent(Intent.ACTION_VIEW, articleUri ?: commentsUri))
         }
```

The diffs are trimmed. Text posts hide the site line, and tapping one opens its discussion page, which is what every tap did before 1.4.

A sturdier fix is to stop parsing Kotlin models with Gson. Gson's README points Android apps to [Kotlin Serialization](https://kotlinlang.org/docs/serialization.html), where "an object can be deserialized only when all its properties are present in the input" unless the property [has a default](https://kotlinlang.org/docs/serialization-customization-options.html#set-default-values-for-optional-properties). A missing `url` would fail at parse time instead of crashing a screen later. That's a bigger change than this bug needed, and the agent didn't make it.

## A test that fails first (6:55)

The agent wrote an instrumented test that parses an Ask-style item with Gson, as the app does, and binds it through the real adapter:

```kotlin
@Test
fun bindsTextPostWithoutUrl() {
    val holder = bind("""{"id":1,"type":"story","title":"Ask HN: Anything?","by":"pg","score":5,"time":0,"descendants":2,"text":"Body"}""")
    assertEquals(View.GONE, holder.binding.site.visibility)
}
```

Instrumented tests need a device. `lim android connect` opened an adb tunnel to the cloud emulator on `127.0.0.1:34253`, and Gradle ran the tests over it. Before trusting the test, the agent stashed its fix and ran it against the 1.4 code:

```bash
lim android connect --id $ID
export ANDROID_SERIAL=127.0.0.1:34253
git stash push -- app/src/main
./gradlew connectedDebugAndroidTest
git stash pop
./gradlew connectedDebugAndroidTest assembleDebug
```

The run against 1.4 took 22 seconds, and 1 of the 2 tests failed with the same `NullPointerException: uriString`. With the fix, both tests passed and the new APK built in 16 seconds.

The agent set `ANDROID_SERIAL` because adb also listed a second emulator, one we were running on the workspace for a separate test. The [adb docs](https://developer.android.com/tools/adb#directingcommands) say to name the target when more than one device is attached. The test results list one device, `limdroid_x86_64_only - 15`, which is the cloud emulator.

## Checking it on the emulator (7:45)

The agent installed the fixed APK and read `lastUpdateTime` from `dumpsys package` to confirm the new build was on the emulator. Then it repeated the report's steps with `screenrecord` running: open the app, wait for Top, tap Ask. The Ask list rendered. It switched tabs, tapped an Ask post, and confirmed that Chrome came to the front showing `news.ycombinator.com/item?id=50200997`. The watcher ended with `reason: terminated`, which was the agent stopping the app, not a crash.

<img src="/blog/debug-android-crash-with-a-coding-agent-fixed.png" alt="The fixed build on the cloud emulator, showing the Ask tab with Ask HN and Tell HN posts and no site line under them" width="300" style="width: 300px;" />

It committed the fix and the test, deleted the emulator, and reported back at 9:47.

## What the agent got wrong

- **It lost 3 minutes and 52 seconds to recording.** Three `record start` timeouts and one `record stop` that hung for two minutes came before it tried `screenrecord`. Switching after the first timeout would have saved most of that.
- **It pulled the first video too early**, while `screenrecord` was still writing it, and had to pull it again.
- **It got the theme name wrong in the test**, `Theme_HNReader` instead of `Theme_HnReader`, and caught it with a grep before the first build.
- **It built locally against the skill's advice.** The Limrun skill says not to use a local SDK. With `lim gradle` blocked, that was the only way to build, and the agent said so in its summary.

It also left a nearby crash alone, on purpose. `StoriesViewModel` only catches `IOException`, so an HTTP error or a deleted item, which the API returns as `null`, would still crash the app on any tab. The agent flagged that in its summary instead of widening the fix, which is the right call for a bug fix.

## Other ways to give an agent an Android emulator

The agent used a Limrun emulator because the workspace has no KVM. These are the other common setups, with prices checked on October 24, 2026.

| Setup | KVM on your side | Price |
|---|---|---|
| Android Studio on your machine | Yes, or the OS hypervisor | Free |
| Cloud VM with nested virtualization | Yes | $0.187/hr (AWS c8i.xlarge) |
| docker-android | Yes | Free; Android 15+ for sponsors |
| Firebase Test Lab | No | $1/hr virtual, $5/hr physical |
| Genymotion SaaS | No | $0.06/min |
| Limrun, used here | No | $0.04/min |

**Your own machine.** Android Studio's emulator is free and fast with [hardware acceleration](https://developer.android.com/studio/run/emulator-acceleration): KVM on Linux, the Hypervisor Framework on macOS, and WHPX on Windows. If Claude Code or Codex runs on your laptop, it can drive that emulator with plain `adb`, and you need none of the rest. It doesn't help an agent running in a cloud sandbox.

**A cloud VM with nested virtualization.** Google Compute Engine [supports it](https://docs.cloud.google.com/compute/docs/instances/nested-virtualization/overview), except on E2, memory-optimized, Arm, and AMD machine types other than N4D, and warns of "a 10% or greater decrease in performance" for CPU-bound work. AWS [added it](https://aws.amazon.com/about-aws/whats-new/2026/02/amazon-ec2-nested-virtualization-on-virtual) on C8i, M8i, and R8i instances in February 2026. A c8i.xlarge, with 4 vCPUs and 8 GiB, is [$0.18742 an hour](https://aws.amazon.com/ec2/pricing/on-demand/) on demand in us-east-1. You install, patch, and expose the emulator yourself.

**docker-android.** [budtmo/docker-android](https://github.com/budtmo/docker-android) packages the emulator with a browser viewer and Appium. Its README says it runs on Ubuntu hosts only, and its quick start passes `--device /dev/kvm`. The free images go up to Android 14; Android 15 and later are in a Pro version for GitHub sponsors. We tried the free Android 14 image on this workspace. With the documented flag, Docker refused to start it: `error gathering device information while adding custom device "/dev/kvm": no such file or directory`. Without the flag, the container started and the emulator exited with `RuntimeError: /dev/kvm cannot be found!`.

**The emulator without acceleration.** On the same workspace, the x86_64 emulator refused to start: `ERROR | x86_64 emulation currently requires hardware acceleration!`. With `-accel off`, it starts on QEMU's software CPU instead. Booting took about 47 minutes on this 8-vCPU workspace, which was also running other work for part of that time. Installing the 6.4 MB APK took 2 minutes 7 seconds, and the app took 1 minute 22 seconds to draw its first frame. Android then showed "Process system isn't responding" and "Settings isn't responding" dialogs, and the app's first API call failed with `Unable to resolve host`. We stopped there.

**Firebase Test Lab.** You upload an APK, and Google runs it on its devices. A [Robo test](https://firebase.google.com/docs/test-lab/android/robo-ux-test) explores the app without a written test, and [results](https://firebase.google.com/docs/test-lab/android/analyzing-results) include "test logs, screenshots, and videos". It costs [$1 per virtual-device hour and $5 per physical-device hour](https://firebase.google.com/docs/test-lab/usage-quotas-pricing), billed by the minute after a free daily quota. Test Lab is deprecated and shuts down on September 30, 2027. Google points users to [Developer Device Platform](https://docs.cloud.google.com/developer-device-platform/release-notes), in preview since August 2026, which [streams physical devices](https://docs.cloud.google.com/developer-device-platform/device-streaming) and has its own agent skill. Test Lab sends results back after a run, so the agent can't poke at the device between steps.

**Genymotion SaaS.** Genymotion runs Android virtual devices in its cloud. You start them from a dashboard or the [`gmsaas` CLI](https://docs.genymotion.com/gmsaas/), whose features include "Securely connect instances to Android Debug Bridge". [Pricing](https://www.genymotion.com/pricing/) is $0.06 per device-minute, or $219 a month per device ($179 billed annually). Genymotion also sells device images on AWS, Google Cloud, Azure, and Alibaba Cloud for $0.60 an hour plus the cloud bill.

For testing on many real phones, the [mobile testing guide](/resources/test-mobile-apps-with-cloud-coding-agents) compares device clouds.

## Cost and limits

The emulator ran for 8 minutes and 34 seconds. Limrun prices Android emulators at [$0.04 a minute](https://limrun.com/pricing) when paid from the credits in its monthly plans, so about 35 cents. Replicas bills mobile testing [at cost](https://docs.replicas.dev/admin/billing), and it counts toward included usage.

Limits that applied to this run:

- 3 active Limrun instances per workspace, and 3 hours at most per instance.
- Emulators are deleted after 3 idle minutes by default. The agent set `--inactivity-timeout 45m`; the maximum is an hour.
- Instances stop when the workspace sleeps, is archived, or is deleted.
- No Limrun Gradle sandboxes, so APKs build on the workspace.
- Mobile testing isn't available to organizations with a signed BAA.

The fix was never run on a Pixel 8. An emulator was enough here because the crash came from the API's data and the app's code. For crashes tied to a device, a GPU, or a vendor's Android build, you still need a real phone.

Every Replicas workspace has the `lim` CLI and Limrun's skills set up. Give an agent your next crash report and ask it to reproduce the crash on an emulator before it changes any code. The [mobile testing docs](https://docs.replicas.dev/features/mobile-testing) cover the setup.

