Native release polish: a usable routing client on Android, a served APK, and 1.0.0 #44

Closed
opened 2026-08-10 22:37:55 +00:00 by jeroen · 0 comments
Owner

Problem

Viberfox has never shipped an Android build, and the reason was invisible: CI ran
cargo fmt --check as the first step of the check job
(.forgejo/workflows/ci.yml), so an unformatted merge ended the job before the
Android lane below it ran. Behind that sat a real compile break — the APK is
built --no-default-features, which drops solo, which makes
SessionMode::Solo and SessionStatus::Starting unconstructed
(crates/viberfox/src/systems/net/session.rs:116,152); [workspace.lints.rust] unused = "deny" turns that into a hard error. deploy-android.yml failed on
every push to main and https://viberfox.garage44.eu/apk/ has been a 404
throughout.

Beyond the build, the phone was missing the things that make a routing client a
routing client, and the desktop carried several defects that make software feel
unreliable:

  • No position source on any native target. LocationWatch::available() was
    cfg!(target_arch = "wasm32") (crates/viberfox/src/platform/location.rs:60),
    so driver mode narrated the avatar — which on a phone cannot move, movement
    being keyboard-only (systems/player/avatar.rs:1223).
  • No text entry. NativeActivity has no IME, so the address bar, geo search,
    bookmark rename, chat and the AI panel could be focused and not typed into.
  • Nothing persisted. user.json was cwd-relative
    (systems/net/user_store.rs:282) and a NativeActivity process starts at /,
    so every save failed and only warned.
  • Msaa::Sample2 applied on Android (systems/player/free_camera.rs:560),
    where Vulkan's guaranteed minimum for framebufferColorSampleCounts is 1|4
    the same class of failure the surrounding comment already documents twice.
  • The flat clipmap rasterised every tile at 512² on every tier, ~310 MB of
    phone residency where the globe path had already concluded 256².
  • Collision was off whenever the Height layer was on: the airborne test read
    display_position, which folds in terrain_lift (systems/net/network.rs:901,
    systems/player/collision.rs:427), and Dutch relief is ~2 m against an
    AIRBORNE_EPS of 0.2. The avatar walked through every wall.
  • One ☆ press permanently removed the HUD and compass via a legacy
    bookmarks.panel_open latch nothing ever cleared (systems/gui/hud.rs:755).
  • while true do end at Luau top level hung the simulator forever and survived
    restart
    (crates/simulator/src/scripting.rs:510) — a pcall body is
    non-yieldable in Luau and mlua silently discards a yield it cannot honour, so
    LOAD_FUEL never fired. The Linux build runs this simulator in-process.
  • Hostile tile bytes could abort the app. mvt-reader reserves per MoveTo, so
    a ~3 MB crafted body requests ~24 TB; three spatial-bucket builders swept
    coordinate ranges near 2.1e9. panic = "abort" makes each an immediate SIGABRT.
  • No LICENSE anywhere, and map attribution reachable only by opening one
    collapsible panel — ODbL and CC BY 4.0 both require attribution a user can find.

Approach

One branch, landed as path-scoped commits:

  • crates/viberfox/src/platform/android.rs (new) — JNI-only Android services: a
    location provider behind the existing LocationWatch seam, keep-screen-on, and
    Intent.ACTION_VIEW. No Java, no dex: android:hasCode="false" is what
    lets this app ship without Gradle, and every push-shaped location API needs a
    Java type we cannot define, so updates are requested with a PendingIntent and
    the fix is read by polling getLastKnownLocation.
  • crates/viberfox/src/systems/gui/onscreen_keyboard.rs (new) — an egui QWERTY
    slab, since the phone has no other keyboard.
  • crates/viberfox/src/quality.rs — grow Budget with msaa, tile_px,
    atmosphere_luts, space_raymarch. Every existing escape hatch is an env var,
    and Android has neither an environment nor a query string, so phone decisions
    must live in the budget or in UI.
  • crates/geo/src/ — bound the tile body and walk the protobuf to compute what
    mvt-reader will reserve before it sees anything; bbox-reject and clamp the
    three bucket builders.
  • crates/simulator/src/ — run the top-level chunk as a fuel-bounded thread,
    bound the per-connection inboxes, check room membership, reject non-finite
    intents, take argon2 out from under the SQLite mutex.
  • systems/nav/, systems/gui/ — navigation UX (reroute robustness, road
    snapping, audio turn cues, GPS quality, phone layout), destination entry from
    search/bookmarks/POIs, a type and radius scale in theme.rs, an About panel.
  • Release plumbing — LICENSE-MIT + LICENSE-APACHE (MIT OR Apache-2.0),
    CHANGELOG.md, 1.0.0, real versionCode/versionName through aapt2 link, a
    window background so cold start is not a black screen, an adaptive launcher
    icon, Play Protect instructions on the install page.

Acceptance criteria

  • cargo check -p viberfox_android --target aarch64-linux-android passes
  • cargo check -p viberfox --no-default-features passes (the APK's config)
  • cargo check -p viberfox --no-default-features --target wasm32-unknown-unknown passes on nightly
  • cargo fmt --check passes for all five crates, and is the last CI step
  • cargo test green for viberfox, viberfox_geo, viberfox_core, viberfox_simulator, big_space
  • deploy-android.yml succeeds on main and https://viberfox.garage44.eu/apk/ serves an APK, its .sha256 and an install page (curl-verified 200, not assumed)
  • Two consecutive deploys install over each other without an uninstall (monotonic versionCode, stable signing cert)
  • LICENSE-MIT and LICENSE-APACHE at the root, license in [workspace.package], both shipped in the tarball and the APK
  • Attribution for OSM/ODbL, VersaTiles/Shortbread, 3DBAG, AHN/PDOK, OVapi, Quaternius and the OFL font reachable in two taps in-app
  • A route can be started from a search result, a bookmark and a map POI without typing
  • Regression tests exist for: the Luau top-level runaway, the tile geometry bomb, non-finite ClientIntent, and the on-screen keyboard's event stream

Verification

cargo check --workspace --all-targets
cargo test -p viberfox --lib && cargo test -p viberfox_geo --lib \
  && cargo test -p viberfox_core --lib && cargo test -p viberfox_simulator
cargo check -p viberfox --no-default-features
cargo check -p viberfox_android --target aarch64-linux-android

The Android check needs an NDK and the aarch64-linux-android target, and the
agent container has neither by default — /usr/local/rustup is root-owned, so
the target has to be added under a copied RUSTUP_HOME.

Two things cannot be verified anywhere in this container and must be done on
real hardware:

  1. One adb install, watching adb logcat -s viberfox:V bevy_render:V wgpu:V: launch, map streams, rotate, home, resume, back. Nothing between "it
    compiles" and "a person uses it" has ever been observed.
  2. The location provider specifically. It rests on the claim that fixes
    caused by our own PendingIntent registration land in the cache we poll.
    That is how AOSP's LocationProviderManager behaves, but if it is wrong the
    symptom is an arrow that never moves — and there is no third dex-free option,
    only GameActivity and Gradle. The four device-dependent claims are written
    into run_location's doc comment.

Anything touching camera, shaders, egui layout or lighting needs cargo shots on
a workstation; this container has no GPU.

Out of scope

  • GameActivity and a Gradle project. The trigger for reconsidering is typing
    on the phone becoming the thing that matters, or the location design failing on
    hardware — not this ticket.
  • Speech guidance. There is no TTS in this tree and native has none; turn
    cues are chimes.
  • Route alternatives, waypoints/vias, and avoid options. Real features, and
    the difference between a routing demo and a trip planner, but each is its own
    piece of work.
  • A second ABI. The published APK stays arm64-v8a; every phone worth
    targeting is arm64 and a second ABI doubles a 28-minute link.
  • The OVapi cleartext problem. Live transit is blocked by Android's default
    at targetSdk 34 and needs either a network-security config or the relay.
  • A .deb/AppImage for Linux. desktop_integration.rs self-installs the
    .desktop entry, so the tarball needs no installer.

Branch: feat/44-native-release-polish

## Problem Viberfox has never shipped an Android build, and the reason was invisible: CI ran `cargo fmt --check` as the *first* step of the `check` job (`.forgejo/workflows/ci.yml`), so an unformatted merge ended the job before the Android lane below it ran. Behind that sat a real compile break — the APK is built `--no-default-features`, which drops `solo`, which makes `SessionMode::Solo` and `SessionStatus::Starting` unconstructed (`crates/viberfox/src/systems/net/session.rs:116,152`); `[workspace.lints.rust] unused = "deny"` turns that into a hard error. `deploy-android.yml` failed on every push to `main` and `https://viberfox.garage44.eu/apk/` has been a 404 throughout. Beyond the build, the phone was missing the things that make a routing client a routing client, and the desktop carried several defects that make software feel unreliable: - **No position source on any native target.** `LocationWatch::available()` was `cfg!(target_arch = "wasm32")` (`crates/viberfox/src/platform/location.rs:60`), so driver mode narrated the avatar — which on a phone cannot move, movement being keyboard-only (`systems/player/avatar.rs:1223`). - **No text entry.** NativeActivity has no IME, so the address bar, geo search, bookmark rename, chat and the AI panel could be focused and not typed into. - **Nothing persisted.** `user.json` was cwd-relative (`systems/net/user_store.rs:282`) and a NativeActivity process starts at `/`, so every save failed and only warned. - **`Msaa::Sample2` applied on Android** (`systems/player/free_camera.rs:560`), where Vulkan's guaranteed minimum for `framebufferColorSampleCounts` is `1|4` — the same class of failure the surrounding comment already documents twice. - **The flat clipmap rasterised every tile at 512² on every tier**, ~310 MB of phone residency where the globe path had already concluded 256². - **Collision was off whenever the Height layer was on**: the airborne test read `display_position`, which folds in `terrain_lift` (`systems/net/network.rs:901`, `systems/player/collision.rs:427`), and Dutch relief is ~2 m against an `AIRBORNE_EPS` of 0.2. The avatar walked through every wall. - **One ☆ press permanently removed the HUD and compass** via a legacy `bookmarks.panel_open` latch nothing ever cleared (`systems/gui/hud.rs:755`). - **`while true do end` at Luau top level hung the simulator forever and survived restart** (`crates/simulator/src/scripting.rs:510`) — a `pcall` body is non-yieldable in Luau and mlua silently discards a yield it cannot honour, so `LOAD_FUEL` never fired. The Linux build runs this simulator in-process. - **Hostile tile bytes could abort the app.** mvt-reader reserves per `MoveTo`, so a ~3 MB crafted body requests ~24 TB; three spatial-bucket builders swept coordinate ranges near 2.1e9. `panic = "abort"` makes each an immediate SIGABRT. - **No LICENSE anywhere**, and map attribution reachable only by opening one collapsible panel — ODbL and CC BY 4.0 both require attribution a user can find. ## Approach One branch, landed as path-scoped commits: - `crates/viberfox/src/platform/android.rs` (new) — JNI-only Android services: a location provider behind the existing `LocationWatch` seam, keep-screen-on, and `Intent.ACTION_VIEW`. **No Java, no dex**: `android:hasCode="false"` is what lets this app ship without Gradle, and every push-shaped location API needs a Java type we cannot define, so updates are requested with a `PendingIntent` and the fix is read by polling `getLastKnownLocation`. - `crates/viberfox/src/systems/gui/onscreen_keyboard.rs` (new) — an egui QWERTY slab, since the phone has no other keyboard. - `crates/viberfox/src/quality.rs` — grow `Budget` with `msaa`, `tile_px`, `atmosphere_luts`, `space_raymarch`. Every existing escape hatch is an env var, and Android has neither an environment nor a query string, so phone decisions must live in the budget or in UI. - `crates/geo/src/` — bound the tile body and walk the protobuf to compute what mvt-reader will reserve *before* it sees anything; bbox-reject and clamp the three bucket builders. - `crates/simulator/src/` — run the top-level chunk as a fuel-bounded thread, bound the per-connection inboxes, check room membership, reject non-finite intents, take argon2 out from under the SQLite mutex. - `systems/nav/`, `systems/gui/` — navigation UX (reroute robustness, road snapping, audio turn cues, GPS quality, phone layout), destination entry from search/bookmarks/POIs, a type and radius scale in `theme.rs`, an About panel. - Release plumbing — `LICENSE-MIT` + `LICENSE-APACHE` (`MIT OR Apache-2.0`), `CHANGELOG.md`, 1.0.0, real `versionCode`/`versionName` through `aapt2 link`, a window background so cold start is not a black screen, an adaptive launcher icon, Play Protect instructions on the install page. ## Acceptance criteria - [ ] `cargo check -p viberfox_android --target aarch64-linux-android` passes - [ ] `cargo check -p viberfox --no-default-features` passes (the APK's config) - [ ] `cargo check -p viberfox --no-default-features --target wasm32-unknown-unknown` passes on nightly - [ ] `cargo fmt --check` passes for all five crates, and is the *last* CI step - [ ] `cargo test` green for `viberfox`, `viberfox_geo`, `viberfox_core`, `viberfox_simulator`, `big_space` - [ ] `deploy-android.yml` succeeds on `main` and `https://viberfox.garage44.eu/apk/` serves an APK, its `.sha256` and an install page (curl-verified 200, not assumed) - [ ] Two consecutive deploys install over each other without an uninstall (monotonic `versionCode`, stable signing cert) - [ ] `LICENSE-MIT` and `LICENSE-APACHE` at the root, `license` in `[workspace.package]`, both shipped in the tarball and the APK - [ ] Attribution for OSM/ODbL, VersaTiles/Shortbread, 3DBAG, AHN/PDOK, OVapi, Quaternius and the OFL font reachable in two taps in-app - [ ] A route can be started from a search result, a bookmark and a map POI without typing - [ ] Regression tests exist for: the Luau top-level runaway, the tile geometry bomb, non-finite `ClientIntent`, and the on-screen keyboard's event stream ## Verification ```bash cargo check --workspace --all-targets cargo test -p viberfox --lib && cargo test -p viberfox_geo --lib \ && cargo test -p viberfox_core --lib && cargo test -p viberfox_simulator cargo check -p viberfox --no-default-features cargo check -p viberfox_android --target aarch64-linux-android ``` The Android check needs an NDK and the `aarch64-linux-android` target, and the agent container has neither by default — `/usr/local/rustup` is root-owned, so the target has to be added under a copied `RUSTUP_HOME`. **Two things cannot be verified anywhere in this container and must be done on real hardware:** 1. **One `adb install`**, watching `adb logcat -s viberfox:V bevy_render:V wgpu:V`: launch, map streams, rotate, home, resume, back. Nothing between "it compiles" and "a person uses it" has ever been observed. 2. **The location provider specifically.** It rests on the claim that fixes caused by our own `PendingIntent` registration land in the cache we poll. That is how AOSP's `LocationProviderManager` behaves, but if it is wrong the symptom is an arrow that never moves — and there is no third dex-free option, only GameActivity and Gradle. The four device-dependent claims are written into `run_location`'s doc comment. Anything touching camera, shaders, egui layout or lighting needs `cargo shots` on a workstation; this container has no GPU. ## Out of scope - **GameActivity and a Gradle project.** The trigger for reconsidering is typing on the phone becoming the thing that matters, or the location design failing on hardware — not this ticket. - **Speech guidance.** There is no TTS in this tree and native has none; turn cues are chimes. - **Route alternatives, waypoints/vias, and avoid options.** Real features, and the difference between a routing demo and a trip planner, but each is its own piece of work. - **A second ABI.** The published APK stays `arm64-v8a`; every phone worth targeting is arm64 and a second ABI doubles a 28-minute link. - **The OVapi cleartext problem.** Live transit is blocked by Android's default at `targetSdk` 34 and needs either a network-security config or the relay. - **A `.deb`/AppImage for Linux.** `desktop_integration.rs` self-installs the `.desktop` entry, so the tarball needs no installer. --- Branch: `feat/44-native-release-polish`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#44
No description provided.