This README contains instructions for building and testing the Android client locally.
Requires Rust and mise to be installed.
mise run setup
mise run buildThis installs Java, Android SDK/NDK, Rust cross-compilation targets, and
creates local.properties. See mise-tasks/setup.sh for details.
To install on a connected device or emulator:
mise run install-phone # connected hardware device
mise run install-emulator # creates/boots an emulator and launches the appBoth tasks build only the cargo target matching the device's ABI (detected via
adb for install-phone, host arch for install-emulator), which is roughly
4x faster than the default all-ABI build.
Useful when your USB connection is flaky. Both flows give you a regular adb
connection, after which mise run install-phone works normally.
Android 11+ (native pairing, persists across reboots):
-
On the device: Settings → Developer options → Wireless debugging → enable.
-
Tap "Pair device with pairing code" — note the IP, port, and 6-digit code.
-
On the host:
adb pair <ip>:<pair-port> <code> # one-off pairing adb connect <ip>:<connect-port> # port shown on the main wireless screen
Android 10 and older (USB seed required, resets on reboot):
-
Connect once over USB and confirm
adb deviceslists the phone. -
Switch the device's adb daemon to TCP and connect:
adb tcpip 5555 adb connect <phone-ip>:5555 # Settings → About phone → Status for the IP
-
Disconnect USB. The connection persists until the phone reboots, after which you'll need to repeat the USB seed step.
If you'd rather not use mise run setup:
-
Install Rust, JDK 17, and the Android SDK (via Android Studio or
sdkmanager). -
Install the NDK version pinned as
ndkVersioninapp/build.gradle.ktsvia Android Studio's SDK Manager orsdkmanager "ndk;<version>". -
Create
local.propertieswithsdk.dir=/path/to/Android/Sdk. -
Add the Rust cross-compilation targets to the toolchain pinned in
rust/rust-toolchain.toml:rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android -
Run
./gradlew assembleDebugto verify.
If you get errors about cargo not being found, make sure ~/.cargo/bin is on
the PATH of whatever launches the build; the Gradle task invokes cargo
directly. For example:
# ~/.zprofile or ~/.bash_profile
export PATH="$HOME/.cargo/bin:$PATH"
Debug builds can stand in for connlib, the portal, or both, so the UI can be
driven without a gateway, a portal or a granted VPN permission. They are two
boolean extras on the normal launch, read by MainActivity before anything
that depends on them:
| Extra | What it stands in for |
|---|---|
mockTunnel |
connlib: the session reports the fixtures the end-to-end tests assert against |
skipPortalAuth |
the portal: signing in answers its own request instead of opening a browser |
In Android Studio, put them in Run → Edit Configurations → Launch Flags on the app configuration:
--ez mockTunnel true --ez skipPortalAuth true
Or by hand:
adb shell am start -n dev.firezone.android/.core.presentation.MainActivity \
--ez mockTunnel true --ez skipPortalAuth trueEither extra can be set to false to exercise the real half: mockTunnel true skipPortalAuth false runs the mocked tunnel against a real sign-in.
The managed-device:* tasks drive the test Device Policy Controller in dpc/
by broadcast, which puts an attached emulator into every state an X.509-managed
device can be in without a human at the screen: a certificate the app may use,
one installed without a grant, several to tell apart, or nothing installed at
all. A corporate-owned device whose policy names the certificate for the app is
policy-alias: the DPC answers the KeyChain itself, and nothing is asked of the
user or configured in the app. The ungranted certificate under a required
managed-config is what a personally-owned device carrying a work profile looks
like, and it is what sends the app to the certificate screen. To see the user
tell certificates apart, install a second one under another alias, from any
PKCS#12 with --file.
mise run //kotlin/android:managed-device:provision
mise run //kotlin/android:managed-device:install-certificate --no-grant
mise run //kotlin/android:managed-device:managed-config --certificate true
mise run //kotlin/android:managed-device:policy-alias --alias firezone-clientprovision makes the DPC the owner of the device, or of a work profile with
--work-profile; a device owner can only be set on an emulator that carries no
accounts, so pick an image without Play Services. The certificate comes from
the shared X.509 tasks, so issue one first:
mise run //:x509:create-ca
mise run //:x509:gen-certificate devicereset gives the device back.
We release from GitHub CI, so this shouldn't be necessary. But if you're looking
to test the release variant locally:
- Download the keystore from 1Pass and save to
app/.signing/keystore.jksdir. - Now you can execute the
*Releasetasks with:
export KEYSTORE_PATH="$(pwd)/app/.signing/keystore.jks"
HISTCONTROL=ignorespace # prevents saving the next line in shell history
KEYSTORE_PASSWORD='keystore_password' KEYSTORE_KEY_PASSWORD='keystore_key_password' ./gradlew assembleReleaseTo stream colored logcat from the connected device/emulator:
mise run logcat
Set ANDROID_SERIAL to disambiguate when multiple devices/emulators are
connected. Pass a tag filterspec after -- to narrow the output, e.g. just the
Rust connlib tag:
mise run logcat -- 'connlib:V *:S'
This will show logs of all levels from the connlib tag and silence logs from other tags (*:S).