- 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
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:
- starts an in-process
StudioHubthat listens on127.0.0.1:<port>on the host - forwards that port to the device with
adb reverse tcp:<port> tcp:<port> - builds the APK through
cargo-makepad(android build -p <package>) - installs the APK with
adb install -r - force-stops any previous instance of the app
- launches the app with
am start, passing the hub address, build id, and crate name as intent extras - waits for the app to connect to the hub (
AppStarted) - settles until the app actually answers a request (see "Startup race fix")
- drives the test through the normal
TestApp/Locator/SelectorAPIs
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-makepadgenerates aMakepadAppJavaActivitythat bridges intoMakepadNative.activityOnCreate. - NativeActivity requires the flag on both sides: the platform crate must
be compiled with
--cfg native_activity(so thenative_activity.rsmodule and theANativeActivity_onCreateentry point are used instead of the Java activity), and the APK must be built withcargo makepad android --native-activity build -p <pkg>.build_android_apkinruntime.rsadds the flag automatically whenconfig.android_native_activityis 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 +nightlyand the NDK target installed foraarch64-linux-android. - The
cargo-makepadbinary 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.rsgained anative_activityargument that flows intorust_build(alongside the profile-basedprefer_dynamicchoice) 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