# Building NetDesk for Android NetDesk's Android app is a Flutter UI (`ui/`) bound, through [flutter_rust_bridge](https://cjycode.com/flutter_rust_bridge/) (FRB v2), to a compiled Rust library. The Rust FFI layer is the **`netdesk-frb`** crate (`frb/`), a `cdylib` that statically links **`netdesk-core`** (signaling, E2E crypto, transport, H.264 decode). On Android the library ships inside the APK/AAB as `lib//libnetdesk_frb.so` and the Dart side `dlopen`s it (the FRB loader looks for the stem `netdesk_frb`). ``` netdesk-frb (+ netdesk-core) --cargo-ndk--> ui/android/app/src/main/jniLibs//libnetdesk_frb.so Flutter (ui/) --gradle-----> release .apk / split .apk / .aab (signed) ``` --- ## The one command From the repo root (`netdesk-native/`), in a shell whose `PATH` contains the MinGW tools, cargo, and Flutter: ```powershell powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 ``` That single command: 1. verifies / auto-provisions the toolchain (cargo-ndk, the rustup Android targets), 2. detects the Android NDK at `C:\dev\android-ndk`, 3. compiles `netdesk-frb` for **arm64-v8a, armeabi-v7a, x86_64** with cargo-ndk and drops `libnetdesk_frb.so` straight into `ui/android/app/src/main/jniLibs//`, 4. builds the release **fat APK**, the **per-ABI split APKs**, and the Play **App Bundle (.aab)**. Useful switches: | Switch | Effect | | --- | --- | | `-Target apk\|split\|aab\|rust\|all` | Build only that deliverable (default `all`; `rust` = just the `.so`). | | `-SkipRust` | Reuse the `.so` already in `jniLibs`, build Flutter only. | | `-Abis arm64-v8a` | Restrict the ABIs (default all three). | | `-GenerateKeystore` | Create the release keystore + `key.properties` first (see below). | | `-MinSdk 21` | Android API the `.so` links against (must be `<=` Flutter `minSdk`). | | `-NdkHome ` | Override the NDK location (default `C:\dev\android-ndk`). | --- ## Toolchain | Component | Version / location used here | | --- | --- | | Rust | stable (cargo 1.99) on `PATH` | | rustup targets | `aarch64-linux-android`, `armv7-linux-androideabi`, `x86_64-linux-android` | | cargo-ndk | 4.1.2 (`cargo install cargo-ndk`) | | Android NDK | r26d (`26.3.11579264`) at `C:\dev\android-ndk` (`ANDROID_NDK_HOME`) | | Android SDK | `%LOCALAPPDATA%\Android\Sdk` | | Flutter | `C:\dev\flutter\bin\flutter.bat` | | JDK (keytool + Gradle) | Android Studio JBR (auto-detected) | | nasm | on `PATH` (needed by `openh264` for the x86_64 ABI) | One-time setup (the build script also does the first three automatically): ```powershell rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android cargo install cargo-ndk # NDK: unzip the Android NDK r26+ to C:\dev\android-ndk (set ANDROID_NDK_HOME) ``` Recommended `PATH` (Git Bash form): ```bash export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:/c/dev/flutter/bin:$PATH" ``` ### What cargo-ndk runs The Rust step is exactly: ``` cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \ -o ui/android/app/src/main/jniLibs --platform 21 \ build --release --package netdesk-frb --lib ``` cargo-ndk sets the per-ABI clang/linker from the NDK, and the `-o` flag copies each produced `libnetdesk_frb.so` into `jniLibs//`. The `.so` are build artifacts (git-ignored) and are regenerated on every run. --- ## Release signing Signing credentials live in `ui/android/key.properties`, which is **git-ignored** (so are `*.keystore` / `*.jks`). A committable template is `ui/android/key.properties.example`. `app/build.gradle.kts` loads `key.properties` when present and signs the release build with it; when it is absent the release build falls back to **debug** signing so `flutter run --release` still works during development. Create the upload keystore once (the password is read from the environment and is **never** written into a committed file): ```powershell $env:NETDESK_KEYSTORE_PASSWORD = '' powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -GenerateKeystore ``` This writes: * `ui/android/app/netdesk-release.keystore` - RSA 4096, alias `netdesk`, 10000-day validity, * `ui/android/key.properties` - with the alias, store file, and the password you supplied (local only; keep it out of version control). > Back up the keystore and remember the password. If you lose either you can no > longer push updates to an app already published under that key (unless it uses > Play App Signing with a separate upload key you can reset). --- ## Outputs After a full build: | Deliverable | Path (under `ui/`) | | --- | --- | | Fat APK (all ABIs) | `build/app/outputs/flutter-apk/app-release.apk` | | Split APKs (per ABI) | `build/app/outputs/flutter-apk/app-arm64-v8a-release.apk`, `-armeabi-v7a-`, `-x86_64-` | | Play App Bundle | `build/app/outputs/bundle/release/app-release.aab` | The fat APK is convenient for side-loading / direct download; the split APKs are smaller per device; the `.aab` is what Google Play requires. --- ## Publishing to Google Play (AAB) 1. Produce the signed bundle: ```powershell powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -Target aab ``` (or run the default `all` target). The bundle is `ui/build/app/outputs/bundle/release/app-release.aab`. 2. In the [Play Console](https://play.google.com/console), create the app, then upload the `.aab` under a release track (internal testing first). 3. Enrol in **Play App Signing** (recommended). Google then holds the *app signing key*; the keystore above becomes your *upload key*. If the upload key is ever compromised you can ask Google to reset it - which is why App Signing is preferred over shipping the raw signing key. 4. `versionCode` / `versionName` come from `ui/pubspec.yaml` (`version: x.y.z+build`). Bump the `+build` number for every Play upload - the Console rejects a duplicate `versionCode`. --- ## Troubleshooting * **"Android NDK not found"** - the script could not find a complete NDK at `C:\dev\android-ndk` (it checks for `source.properties` + `toolchains\llvm\prebuilt`). Finish extracting the NDK, set `ANDROID_NDK_HOME`, or pass `-NdkHome `. Use `-SkipRust` to build only the Flutter layer against an already-built `.so`. * **x86_64 ABI fails in `openh264`** - that ABI needs `nasm` on `PATH`. arm64-v8a and armeabi-v7a do not. Drop x86_64 with `-Abis arm64-v8a,armeabi-v7a` if you only ship to real devices. * **Gradle wants to download an NDK** - `app/build.gradle.kts` points AGP at `ANDROID_NDK_HOME` when it is set, so the prebuilt (already stripped) `.so` are packaged without a second NDK download. Make sure the env var is set (the build script sets it). * **`UnsatisfiedLinkError: libnetdesk_frb.so`** at runtime - the `.so` for the device's ABI is missing from `jniLibs`. Re-run the Rust step (`-Target rust`) for that ABI, or confirm the FRB loader stem is still `netdesk_frb`.