---
name: android-dev
description: Build Android apps from the ARM64 Oracle server — SDK setup, Gradle/AGP compatibility, Box64 for AAPT2, common build errors, and ADB sideload to connected devices.
category: software-development
metadata:
  hermes:
    emoji: "📱"
---

# Android Development on ARM64 Oracle Server

Build Android APKs on the ARM64 Oracle Cloud server without Android Studio.

## Environment

- Ubuntu 24.04 ARM64 (Oracle Cloud)
- Java 17 OpenJDK: `sudo apt install -y openjdk-17-jdk-headless`
- Box64 for x86_64 binaries (AAPT2): `sudo apt install -y box64`
- Gradle: download binary to `/opt/gradle-8.11.1`, symlink to `/usr/local/bin/gradle`
- Android SDK: `/opt/android-sdk/` owned by `ubuntu`

## Initial Setup

```bash
# 1. Java
sudo apt install -y openjdk-17-jdk-headless

# 2. Box64 (CRITICAL for ARM64 — AAPT2 is x86_64 only)
sudo apt install -y box64 unzip

# 3. Android SDK command-line tools
sudo mkdir -p /opt/android-sdk/cmdline-tools
sudo chown -R ubuntu:ubuntu /opt/android-sdk
cd /tmp
curl -sL -o cmdline-tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
unzip -q cmdline-tools.zip
mv cmdline-tools /opt/android-sdk/cmdline-tools/latest

# 4. Install SDK components
export ANDROID_HOME=/opt/android-sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
yes | sdkmanager --licenses
sdkmanager "platform-tools" "platforms;android-36" "build-tools;34.0.0"

# 5. Gradle (AGP 8.9.x requires Gradle 8.11.1+)
curl -sL -o /tmp/gradle.zip "https://services.gradle.org/distributions/gradle-8.11.1-bin.zip"
sudo unzip -q /tmp/gradle.zip -d /opt
sudo ln -sf /opt/gradle-8.11.1/bin/gradle /usr/local/bin/gradle
```

## Project Setup

Every project needs `local.properties`:
```
sdk.dir=/opt/android-sdk
```

Every project needs `gradle.properties`:
```
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m
```

## Gradle/AGP Version Compatibility

| AGP | Min Gradle | compileSdk |
|-----|-----------|------------|
| 8.2.0 | 8.5 | 34 |
| 8.9.1 | 8.11.1 | 35-36 |

**PITFALL:** Health Connect `1.1.0` requires AGP 8.9.1+ AND compileSdk 36. If you get "requires libraries to compile against version 36", install `platforms;android-36` and set `compileSdk = 36`.

## Common Build Errors

### `android.useAndroidX` not enabled
```
> Configuration `:app:debugRuntimeClasspath` contains AndroidX dependencies, but `android.useAndroidX` is not enabled
```
Fix: create `gradle.properties` with `android.useAndroidX=true`.

### AAPT2 daemon startup failed
AAPT2 is x86_64; ARM64 needs Box64: `sudo apt install -y box64`.

### Gradle version too old
```
Minimum supported Gradle version is 8.11.1. Current version is 8.10.2.
```
Fix: update `gradle/wrapper/gradle-wrapper.properties` distributionUrl.

### compileSdk too low
```
requires libraries to compile against version 36; currently compiled against android-34
```
Fix: `sdkmanager "platforms;android-36"` and set `compileSdk = 36`.

## Health Connect API Gotchas (1.1.0 vs earlier)

Health Connect 1.1.0 (AGP 8.9.1+) changed the API surface significantly from alpha/beta versions:

- **Aggregate response types:** `StepsRecord.COUNT_TOTAL` returns `Long` (compare with `> 0L`), `ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL` returns `Energy` (use `.inKilocalories`), not raw Ints.
- **Sleep:** `SleepSessionRecord` no longer has `.stage` property. Stage info moved to `SleepStageRecord`.
- **Exercise:** `ExerciseSessionRecord.exerciseType` returns an Int constant, not a nullable object. Use `.toString()` instead of `?.name`.
- **ExerciseSegment:** `.segmentType` also returns Int constant. `.repetitionsCount` may not exist in some builds.
- **Nutrition:** `NutritionRecord.time` is now `startTime`/`endTime`. `.calories` renamed to `.energy`. `.biometric` removed. Use `.totalCarbohydrate`, `.totalFat`, `.protein` with `.inGrams`.
- **Hydration:** `HydrationRecord.time` → `startTime`. `.title` removed.
- **BloodPressure:** `bodyPosition` and `measurementLocation` are Int constants (not nullable enums with `.name`). Use `.toString()`.
- **Mindfulness:** Requires `@OptIn(ExperimentalHealthConnectApi::class)`. Skip if not needed.

**Rule:** When porting Health Connect code between versions, prefer `.toString()` over `.name` and use the explicit unit accessors (`.inKilocalories`, `.inGrams`, `.inMillimetersOfMercury`).

## ADB Sideload

```bash
# Connect to device over Tailscale
adb connect <tailscale_ip>:<wireless_debug_port>
adb devices  # verify "device" status

# Install APK
adb install app/build/outputs/apk/debug/app-debug.apk
```

For device-specific ADB-over-Tailscale instructions (pairing, common operations like reading notifications and listing packages, the only working Health Connect intent, and pitfalls), see `references/adb-over-tailscale.md`. Absorbed from the former `pixel-6a` standalone skill.

## One-shot build + install + launch (no user interaction needed)

The standard pattern when you've built an APK and want the user to start
using it without a single permission dialog or app icon tap:

```bash
# 1. Build
ANDROID_HOME=/opt/android-sdk gradle assembleDebug

# 2. Install (-r re-installs, keeping data)
adb -s <ip:port> install -r app/build/outputs/apk/debug/app-debug.apk

# 3. Pre-grant runtime permissions (skips the "Allow" dialogs)
adb -s <ip:port> shell pm grant <package> android.permission.BLUETOOTH_CONNECT
adb -s <ip:port> shell pm grant <package> android.permission.BLUETOOTH_SCAN
adb -s <ip:port> shell pm grant <package> android.permission.ACCESS_FINE_LOCATION
adb -s <ip:port> shell pm grant <package> android.permission.ACCESS_COARSE_LOCATION

# 4. Launch the main activity
adb -s <ip:port> shell am start -n <package>/.MainActivity
```

If your APK uses `targetSdk >= 33` and the user already has an older
install, the `-r` flag keeps app data but you still need to re-grant the
runtime permissions (Android revokes them across upgrades in some cases).
The user is right to be angry if you ask them to tap "Allow" three times
when adb could have done it from here.

## Common Java compile errors

### `variable X is already defined` in nested `new byte[]{...}` literals

Java 17's `javac` will reject this:
```java
private void cmdColor(int r, int g, int b) {
    byte[] a = new byte[]{ ..., (byte)(b & 0xFF), ... };
    byte[] b = new byte[]{ ..., (byte)(b & 0xFF), ... };  // ERROR: b is a parameter AND a local
    byte[] c = new byte[]{ ..., (byte)(b & 0xFF), ... };  // ERROR: same
}
```

The error message says "bad operand types for binary operator '&' — first
type: byte[], second type: int" because the compiler resolves the inner
`b` to the local `byte[]` variable, not the parameter. Fix: rename your
local byte arrays to `v1`, `v2`, `v3` (or any non-conflicting name).

## Project Structure Reference

Minimal Android project:
```
project/
├── build.gradle.kts          # Top-level: AGP + Kotlin plugin versions
├── settings.gradle.kts       # pluginManagement + dependencyResolutionManagement
├── gradle.properties         # android.useAndroidX=true + JVM args
├── local.properties          # sdk.dir=/opt/android-sdk
├── gradle/wrapper/
│   └── gradle-wrapper.properties  # distributionUrl with Gradle version
└── app/
    ├── build.gradle.kts      # namespace, compileSdk, dependencies
    └── src/main/
        ├── AndroidManifest.xml
        ├── java/<package>/
        │   └── *.kt
        └── res/
            ├── layout/
            │   └── activity_main.xml
            └── values/
                └── strings.xml
```

## Building

```bash
cd project
./gradlew assembleDebug --no-daemon
# APK at: app/build/outputs/apk/debug/app-debug.apk
```

## Release builds: `apksigner` is at `/opt/android-sdk/build-tools/34.0.0/`, NOT `~/android-sdk`

When a project has no `signingConfig` in `app/build.gradle.kts` (very common for Chicho's ad-hoc APKs — `health-bridge`, `ble-led-controller`, etc.), `./gradlew assembleRelease` produces an **unsigned** APK at `app/build/outputs/apk/release/app-release-unsigned.apk` (note the `-unsigned` suffix). There is NO `app-release.apk` — the build script will silently fall back to the unsigned output even when you copy from a path that "should" exist. The standard "debug" path `assembleDebug` always produces a signed APK at `app/build/outputs/apk/debug/app-debug.apk` (signed with `~/.android/debug.keystore`), so prefer that for sideload unless you have a real release keystore.

To sign a release APK manually with the debug keystore (Chicho's convention for personal APKs):

```bash
# 1. Build (produces app-release-unsigned.apk)
ANDROID_HOME=/opt/android-sdk ./gradlew assembleRelease --no-daemon

# 2. Sign with apksigner from the SDK's build-tools, NOT $PATH
/opt/android-sdk/build-tools/34.0.0/apksigner sign \
  --ks ~/.android/debug.keystore \
  --ks-pass pass:android \
  --key-pass pass:android \
  --ks-key-alias androiddebugkey \
  --out <dest>.apk \
  app/build/outputs/apk/release/app-release-unsigned.apk

# 3. Verify it landed (sha256 changes; "Verification succesful" is apksigner's spelling)
sha256sum <dest>.apk
```

`/opt/android-sdk/build-tools/34.0.0/apksigner` — that's the path. `~/android-sdk/...` does NOT exist on this Oracle box. The Android SDK lives at `/opt/android-sdk`, not under `$HOME`. Common mistakes:

- `apksigner` "command not found" → use the absolute path above (or `find /opt/android-sdk -name apksigner`)
- `./gradlew assembleRelease` succeeds but `cp app/build/outputs/apk/release/app-release.apk …` fails → the file is `app-release-unsigned.apk`. Always `ls` the output dir after a release build.
- Signing produces "no certificates matched" → wrong keystore alias. Default debug keystore alias is `androiddebugkey`, not `debug`.

## References

- Load `health-bridge` for Health Connect bridge architecture, permissions, APK rebuild/deploy, and Pixel Health Bridge troubleshooting.
- `references/adb-over-tailscale.md` — full ADB-over-Tailscale pattern (pairing, common operations, Health Connect intent). Absorbed from the former `pixel-6a` standalone skill.
- `references/ble-led-controller-case-study.md` — case study for building the BLE LEDDMX-03 LED-strip controller APK. Full reverse-engineered protocol, build/grant/launch sequence, strict device-name filter, persistence pattern, gravity-based dimmer, banderazo tricolor loop, and the stop-all-animations pitfall. Absorbed from the former `ble-led-controller-android-apk` standalone skill.
- `references/leddmx-protocol-map.md` — the full opcode-to-byte map for the LEDDMX-03 family (and all sibling model variants) extracted from the `com.ledlamp` smali. Load before re-decompiling the reference APK.
- `scripts/rebuild-health-bridge.sh` — One-shot rebuild + deploy the Health Bridge APK.
- `templates/ble-led-controller-MainActivity.java` — copy-and-modify starting point. Already has BLE GATT setup, model detection, write helpers, brightness SeekBar, bandera tricolor, persistent device, and the strict device-name filter. Includes the LEDDMX-03 opcode constants so a new command only needs the byte array.

## Skills absorbed

This umbrella merges the following former skills (archived):
- `pixel-6a` — the device-specific ADB-over-Tailscale recipe (hardcoded to one Pixel 6a with specific IPs and a specific OpenClaw gateway). The reusable pairing/operations pattern was extracted to `references/adb-over-tailscale.md`; the device-specific data (IPs, device ID, installed-app inventory, OpenClaw gateway config) was dropped as session-specific.
- `ble-led-controller-android-apk` — the full case study for replacing the bad stock LEDDMX-03 app with a clean native controller. The case study moved to `references/ble-led-controller-case-study.md`; the protocol map to `references/leddmx-protocol-map.md`; the working Java template to `templates/ble-led-controller-MainActivity.java`.
