# Building the Android app

## What happened on 12 September 2026, and how to avoid it

The machine was restarted while a Gradle build was running. That caused
five consecutive build failures, each with a different error message, and
took several hours to work through. Recording it because the pattern is
easy to repeat and the symptoms do not point at the cause.

### The root cause

**A hard restart mid-build zero-fills files that were open for writing.**
Windows leaves them the right size with null bytes inside, so nothing
looks obviously wrong — until something tries to read them.

Three separate caches were damaged, and each produced a different,
misleading error:

| Damaged | Error it produced | Looks like |
|---|---|---|
| `~/.gradle/caches/*/transforms` | "The contents of the immutable workspace have been modified… or could be the result of disk corruption" | A Gradle bug |
| `~/.gradle/caches/modules-2` (downloaded jars) | "Execution failed for BuildToolsApiClasspathEntrySnapshotTransform" | A dependency version conflict |
| `node_modules/*/android/.cxx` (CMake metadata) | "MalformedJsonException … at line 1 column 1" | A corrupt build script |

The third one was verified directly: `collection-1.0.0.jar` was a
truncated file that `unzip -t` rejected as not an archive at all, and the
CMake `reply/*.json` files contained nothing but whitespace.

### The fix

**Clear everything the restart could have touched, in one go.** Fixing
them one at a time just moves the error:

```bash
# 1. Stop anything holding a lock
#    (PowerShell) Get-Process java | Stop-Process -Force

# 2. Gradle's caches — the big one, ~3.5 GB, will re-download
rm -rf ~/.gradle/caches

# 3. Native build metadata, per module
find node_modules -maxdepth 3 -name ".cxx" -type d -exec rm -rf {} +

# 4. The app's own build output and lock directory
rm -rf apps/mobile/android/app/build apps/mobile/android/.gradle
```

The Android SDK itself (`%LOCALAPPDATA%\Android\Sdk`) is **not** affected
— the NDK and build-tools downloads survive, and they are the slowest
part to re-fetch.

### How to check before assuming

```bash
# Is a cached jar actually a valid archive?
unzip -t ~/.gradle/caches/modules-2/files-2.1/<path-to>.jar

# Are the CMake metadata files real, or zero-filled?
head -c 40 node_modules/expo-modules-core/android/.cxx/Debug/*/reply/*.json
```

Blank output or "cannot find zipfile directory" means corruption, not a
configuration problem.

### Avoiding it

- Let a running build finish, or stop it with Ctrl-C, before restarting.
- `./gradlew --stop` before a planned reboot shuts the daemon down
  cleanly.

---

## Two mistakes that were mine, not the restart's

**1. Killing a build that looked stalled.** Gradle's output was being
buffered by a `| tail` pipe, so the log looked frozen while the build was
fine. Killing it left a lock behind (`android/.gradle/…/buildLogic.lock`)
and the next run failed with "Timeout waiting to lock build logic queue".

Write build output to a file and tail *that* — do not pipe it through
`tail` and wonder why nothing appears.

**2. Preserving "useful" cache after the restart.** 60 MB of `.cxx`
output had survived, so it was kept. It was corrupt, and caused failure
number five. After a hard restart, treat every build cache as suspect.

---

## Building

### Architecture matters

The default builds four CPU architectures. Build only the one you need —
it is four times faster, and one of the extra three (`arm64-v8a`) was
crashing clang on this machine.

```bash
cd apps/mobile/android

# For the Android emulator (x86_64)
./gradlew assembleDebug -PreactNativeArchitectures=x86_64

# For a real phone (almost always arm64-v8a)
./gradlew assembleDebug -PreactNativeArchitectures=arm64-v8a

# For an APK that runs on both — larger, but only build it once
./gradlew assembleDebug -PreactNativeArchitectures=x86_64,arm64-v8a
```

Check what a connected device needs:

```bash
adb shell getprop ro.product.cpu.abi
```

The APK lands at
`apps/mobile/android/app/build/outputs/apk/debug/app-debug.apk`.

### Installing on a phone

A debug APK is signed with a local debug key, so Android will warn that
it is from an unknown source. That is expected for a build that has not
been through the Play Store.

```bash
# Over USB, with developer options and USB debugging enabled
adb install -r app-debug.apk
```

Or copy the APK to the phone and open it, allowing installation from
unknown sources when prompted.

### What will not work off your machine

The app points at `apiBaseUrl` in `app.json`, which is
`http://localhost:4000/api`. The code rewrites `localhost` to `10.0.2.2`
on Android, which is the **emulator's** route back to the host — a real
phone has no such route.

So on a physical device the app needs one of:

- Your machine's LAN address (`http://192.168.x.x:4000/api`), with both
  on the same network
- A tunnel (`ngrok http 4000`) if they are not
- A deployed API

Without it the app opens and renders, but every screen shows the "could
not load" state. That is the app behaving correctly, not a build problem.

### Push notifications

They require a physical device — `enablePush()` checks `Device.isDevice`
and refuses on an emulator, because Expo push tokens cannot be issued to
one. Everything else works on the emulator.
