Files
firezone/kotlin/android
Thomas Eizinger faeb958882 refactor: use UniFFI for Android FFI (#9415)
To make our FFI layer between Android and Rust safer, we adopt the
UniFFI tool from Mozilla. UniFFI allows us to create a dedicated crate
(here `client-ffi`) that contains Rust structs annotated with various
attributes. These macros then generate code at compile time that is
built into the shared object. Using a dedicated CLI from the UniFFI
project, we can then generate Kotlin bindings from this shared object.

The primary motivation for this effort is memory safety across the FFI
boundary. Most importantly, we want to ensure that:

- The session pointer is not used after it has been free'd
- Disconnecting the session frees the pointer
- Freeing the session does not happen as part of a callback as that
triggers a cyclic dependency on the Rust side (callbacks are executed on
a runtime and that runtime is dropped as part of dropping the session)

To achieve all of these goals, we move away from callbacks altogether.
UniFFI has great support for async functions. We leverage this support
to expose a `suspend fn` to Android that returns `Event`s. These events
map to the current callback functions. Internally, these events are read
from a channel with a capacity of 1000 events. It is therefore not very
time-critical that the app reads from this channel. `connlib` will
happily continue even if the channel is full. 1000 events should be more
than sufficient though in case the host app cannot immediately process
them. We don't send events very often after all.

This event-based design has major advantages: It allows us to make use
of `AutoCloseable` on the Kotlin side, meaning the `session` pointer is
only ever accessed as part of a `use` block and automatically closed
(and therefore free'd) at the end of the block.

To communicate with the session, we introduce a `TunnelCommand` which
represents all actions that the host app can send to `connlib`. These
are passed through a channel to the `suspend fn` which continuously
listens for events and commands.

Resolves: #9499
Related: #3959

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
2025-06-17 21:48:34 +00:00
..
2024-03-08 01:55:27 +00:00

Firezone Android client

This README contains instructions for building and testing the Android client locally.

Dev Setup

  1. Install Rust

  2. Install Android Studio

  3. Install your JDK 17 of choice. We recommend just updating your CLI environment to use the JDK bundled in Android Studio to ensure you're using the same JDK on the CLI as Android Studio.

  4. Install the Android SDK through Android Studio.

    • Open Android studio, go to Android Studio > Preferences
    • Search for sdk
    • Find the Android SDK nav item under System Settings and select
    • Click the Edit button next to the Android SDK Location field
    • Follow the steps presented to install Android SDK
  5. Install NDK using Android Studio

    To see which version is installed, make sure to select the Show Package Details checkbox in the Android SDK settings page in Android Studio

    Android SDK Tools

    Make sure the correct NDK version is installed by looking at: ./app/build.gradle.kts

  6. Set the following properties in your local.properties file:

    sdk.dir=/Users/<username>/Library/Android/sdk
    
  7. Make sure the following Rust targets are installed into the correct toolchain.

    aarch64-linux-android
    arm-linux-androideabi
    armv7-linux-androideabi
    i686-linux-android
    x86_64-linux-android
    

    Ensure you've activated the correct toolchain version for your local environment with rustup default <toolchain> (find this from the root /rust/rust-toolchain.toml file), then run:

    rustup target add aarch64-linux-android arm-linux-androideabi armv7-linux-androideabi i686-linux-android x86_64-linux-android
    
  8. Perform a test build: ./gradlew assembleDebug.

If you get errors about rustc or cargo not being found, it can help to explicitly specify the path to these in your shell environment. For example:

# ~/.zprofile or ~/.bash_profile
export RUST_ANDROID_GRADLE_RUSTC_COMMAND=$HOME/.cargo/bin/rustc
export RUST_ANDROID_GRADLE_CARGO_COMMAND=$HOME/.cargo/bin/cargo

Release Setup

We release from GitHub CI, so this shouldn't be necessary. But if you're looking to test the release variant locally:

  1. Download the keystore from 1Pass and save to app/.signing/keystore.jks dir.
  2. Download firebase credentials from 1Pass and save to app/.signing/firebase.json
  3. Now you can execute the *Release tasks with:
export KEYSTORE_PATH="$(pwd)/app/.signing/keystore.jks"
export FIREBASE_CREDENTIALS_PATH="$(pwd)/app/.signing/firebase.json"
HISTCONTROL=ignorespace # prevents saving the next line in shell history
 KEYSTORE_PASSWORD='keystore_password' KEYSTORE_KEY_PASSWORD='keystore_key_password' ./gradlew assembleRelease

Logs

To see all connlib related logs via ADB use:

adb logcat --format color "connlib *:S"

This will show logs of all levels from the connlib tag and silence logs from other tags (*:S).