# makepad_test on Android How `makepad_test` runs Makepad UI tests on real Android devices, and the steps that were taken to get it working end-to-end. This is the Android companion to [GUIDE.md](./GUIDE.md), which covers the desktop headless and visible-Studio modes. ## What "Android mode" does When `MAKEPAD_TEST_ANDROID=1` is set, the test runtime: 1. starts an in-process `StudioHub` that listens on `127.0.0.1:` on the host 2. forwards that port to the device with `adb reverse tcp: tcp:` 3. builds the APK through `cargo-makepad` (`android build -p `) 4. installs the APK with `adb install -r` 5. force-stops any previous instance of the app 6. launches the app with `am start`, passing the hub address, build id, and crate name as intent extras 7. waits for the app to connect to the hub (`AppStarted`) 8. settles until the app actually answers a request (see "Startup race fix") 9. drives the test through the normal `TestApp` / `Locator` / `Selector` APIs The app dials `127.0.0.1:` on the device; `adb reverse` maps that back to the host listener, so no separate device-side network setup is needed. ## Two runtime modes | Mode | Activity launched | Platform build | When | |------|------------------|----------------|------| | Legacy Java | `dev.makepad./.MakepadApp` | no `--cfg native_activity` | default | | NativeActivity | `/android.app.NativeActivity` | `--cfg native_activity` + `--native-activity` build flag | `MAKEPAD_TEST_NATIVE_ACTIVITY=1` | - **Legacy Java** is the default. `cargo-makepad` generates a `MakepadApp` Java `Activity` that bridges into `MakepadNative.activityOnCreate`. - **NativeActivity** requires the flag on both sides: the platform crate must be compiled with `--cfg native_activity` (so the `native_activity.rs` module and the `ANativeActivity_onCreate` entry point are used instead of the Java activity), and the APK must be built with `cargo makepad android --native-activity build -p `. `build_android_apk` in `runtime.rs` adds the flag automatically when `config.android_native_activity` is set. The launch is wired up in `adb_launch` in `libs/makepad_test/src/runtime.rs`: ```text am start -n -e makepad.STUDIO_HOST 127.0.0.1: -e makepad.STUDIO_BUILD -e makepad.STUDIO_CRATE ``` ## Environment variables | Variable | Purpose | Default | |----------|---------|---------| | `MAKEPAD_TEST_ANDROID` | enable Android mode (any truthy value) | unset (off) | | `MAKEPAD_TEST_DEVICE` | adb device serial (`-s `) | unset (default adb device) | | `MAKEPAD_TEST_ADB` | path to the adb binary | `adb` on `PATH` | | `MAKEPAD_TEST_ANDROID_PORT` | host hub port / adb reverse port | `8001` | | `MAKEPAD_TEST_NATIVE_ACTIVITY` | use NativeActivity instead of legacy Java | unset (legacy) | | `MAKEPAD_WORKSPACE_ROOT` | workspace root holding `tools/cargo_makepad` | auto-detected by walking up from the manifest dir | | `MAKEPAD_STUDIO_HUB_DEBUG` | print every hub child line (build + app output) | unset | The hub binds `127.0.0.1:` and may fall back to a different port if `8001` is already taken (for example by a real Studio). `start_android_app` reads the port the hub actually bound (`connection.studio_addr()`) and routes `adb reverse` and the intent extras at that port, never a hardcoded one. ## Build system requirements - Android builds use the **nightly** toolchain: the platform crate needs `cargo +nightly` and the NDK target installed for `aarch64-linux-android`. - The `cargo-makepad` binary must exist at `/target/release/cargo-makepad`: `cargo build --release -p cargo-makepad`. - The APK lands at `target/android/makepad-android-apk//apk/.apk`. - `tools/cargo_makepad/src/android/compile.rs` gained a `native_activity` argument that flows into `rust_build` (alongside the profile-based `prefer_dynamic` choice) and is plumbed through both APK build call sites. Sanity-compile checks that must stay green: ```bash # legacy Java cfg cargo +nightly check -p makepad-platform --target aarch64-linux-android # native-activity cfg RUSTFLAGS="--cfg native_activity" cargo +nightly check -p makepad-platform --target aarch64-linux-android ``` ## Device setup ```bash export MAKEPAD_TEST_ADB="/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" export MAKEPAD_TEST_DEVICE="RF8Y103NERA" # your device serial $MAKEPAD_TEST_ADB devices # must list the device $MAKEPAD_TEST_ADB -s "$MAKEPAD_TEST_DEVICE" wait-for-device ``` - The device must be **authorized** (accept the USB debugging prompt). - Keep the screen on during the run: `adb -s shell svc power stayon true`. - On some devices a screen-off can still slow or starve the app; test with the screen awake. ## Running the tests Legacy Java mode (default): ```bash MAKEPAD_TEST_ANDROID=1 \ MAKEPAD_TEST_DEVICE="RF8Y103NERA" \ MAKEPAD_TEST_ADB="/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \ cargo test --release -p makepad-example-counter --test ui -- --test-threads=1 ``` NativeActivity mode: ```bash MAKEPAD_TEST_ANDROID=1 \ MAKEPAD_TEST_NATIVE_ACTIVITY=1 \ MAKEPAD_TEST_DEVICE="RF8Y103NERA" \ MAKEPAD_TEST_ADB="/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \ cargo test --release -p makepad-example-counter --test ui -- --test-threads=1 ``` `--test-threads=1` is required: each test owns the shared hub port and the serialized app session. Progress is printed to stderr with `[makepad-test] Android: ...` lines as each phase completes (forward, build, install, force-stop, launch, connect, responsive). ## Expected results Recorded runs on the reconciled tree: | Mode | Device | Result | Time | |------|--------|--------|------| | Native | RF8Y103NERA (SM-A165F) | 2 passed | 515.71s | | Native | R28M52LJP2Y (SM-A6060), screen off | 2 passed | 179.65s | | Legacy | R28M52LJP2Y (SM-A6060) | 2 passed | 361.76s | | Legacy | RF8Y103NERA (SM-A165F) | 2 passed | 957.16s | | Native | RF8Y103NERA (SM-A165F) | 2 passed | 520.70s | The APK build dominates the wall time; app install and test execution are the small remainder. ## The startup race fix (settle step) **Symptom:** the app connected (`AppStarted`) but the first widget query or click was lost, failing the test with a timeout on the first interaction. **Root cause:** the websocket connects on a background thread before the app's event loop is up. A cold start can answer the handshake well before it can service hub requests. Legacy Java starts are the slowest: the first frame (and with it the main loop that drains requests) only comes after the `SurfaceView` surface materializes, which on a first launch after install can exceed the per-request `ACTION_TIMEOUT` (10s). **Fix:** after `wait_for_android_app_started`, call `wait_for_android_app_responsive` (`runtime.rs:1754`). It repeatedly sends `ClientToHub::WidgetTreeDump` and waits for a matching `WidgetTreeDump` reply, with a per-attempt `ACTION_TIMEOUT` and an overall `ANDROID_STARTUP_TIMEOUT` (120s) deadline. The test body's first query only starts once the app has actually answered a request, closing the boot race. ## Debugging Enable hub transport diagnostics, which echo every child stdout/stderr line (APK build output, app logs, protocol messages): ```bash MAKEPAD_STUDIO_HUB_DEBUG=1 ... cargo test --release -p makepad-example-counter --test ui -- --test-threads=1 --nocapture ``` Failure artifacts are written to `target/makepad_test///` (`failure.txt`, `failure-screenshot.png`, logs), exactly like desktop mode. ## Troubleshooting | Symptom | Likely cause / fix | |---------|--------------------| | `adb: error: device '' not found` | device disconnected; reconnect USB and `adb devices` | | `device unauthorized` | accept the USB debugging prompt on the device | | `timed out waiting for Android app to connect to hub` | wrong port / stale `adb reverse`; the hub binds a fallback port, so make sure adb and the app use the actually-bound port (already handled in code) | | `timed out waiting for Android app to become responsive` | very slow cold start; raise `ANDROID_STARTUP_TIMEOUT`, keep the screen on, or rerun once warm | | `cargo-makepad not found at ...` | `cargo build --release -p cargo-makepad` first | | test fails only on the very first launch after install | known cold-start surface race; rerun warm, or run the legacy case twice | ## Current limitations - one device at a time (`-s ` targets a single device) - one app session per test (serial suite with `--test-threads=1`) - the APK build happens inside the test process, so the first test of a suite is the slow one; subsequent tests reuse the built APK