makepad/libs/makepad_test/ANDROID.md
andodeki 477421e314 nigig: NIGIG test-mode forwarding, custom manifest hook, ortho camera support
- makepad_test/runtime.rs: forward NIGIG_TEST_MODE from host env to the
  Android app via 'am start' intent extra; add wait_timeout (60s) used by
  wait_visible/wait_hidden/wait_count; make query_widgets tolerant of
  snapshot timeouts; grant READ_CONTACTS during adb setup
- makepad-platform android_jni.rs: read makepad.NIGIG_TEST_MODE intent
  extra and surface it as the NIGIG_TEST_MODE env var via apply_studio_env
- cargo_makepad compile.rs: support verbatim custom AndroidManifest.xml in
  addition to the templated variant
- makepad-xr xr_root.rs: add ortho camera controls (ortho, ortho_height,
  min/max), derive Debug on XrCamera
- docs: ANDROID.md and DESKTOP_VISIBLE.md for makepad_test
2026-09-03 10:55:00 +03:00

8.6 KiB

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, 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:<port> on the host
  2. forwards that port to the device with adb reverse tcp:<port> tcp:<port>
  3. builds the APK through cargo-makepad (android build -p <package>)
  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:<port> 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.<pkg>/.MakepadApp no --cfg native_activity default
NativeActivity <pkg>/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 <pkg>. 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:

am start -n <activity>
  -e makepad.STUDIO_HOST 127.0.0.1:<port>
  -e makepad.STUDIO_BUILD <build_id>
  -e makepad.STUDIO_CRATE <package>

Environment variables

Variable Purpose Default
MAKEPAD_TEST_ANDROID enable Android mode (any truthy value) unset (off)
MAKEPAD_TEST_DEVICE adb device serial (-s <serial>) 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:<port> 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 <workspace>/target/release/cargo-makepad: cargo build --release -p cargo-makepad.
  • The APK lands at target/android/makepad-android-apk/<pkg_underscored>/apk/<pkg_underscored>.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:

# 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

export MAKEPAD_TEST_ADB="<repo>/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 <serial> 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):

MAKEPAD_TEST_ANDROID=1 \
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \
cargo test --release -p makepad-example-counter --test ui -- --test-threads=1

NativeActivity mode:

MAKEPAD_TEST_ANDROID=1 \
MAKEPAD_TEST_NATIVE_ACTIVITY=1 \
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
MAKEPAD_TEST_ADB="<repo>/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):

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/<package>/<test>/ (failure.txt, failure-screenshot.png, logs), exactly like desktop mode.

Troubleshooting

Symptom Likely cause / fix
adb: error: device '<serial>' 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 <serial> 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