3 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5 5f0fe7b403 Give the app a real icon, adaptive on Android
🚴 Build and Test BikeControl / Workspace tests (push) Successful in 10m18s
Build & Release / Run tests (push) Successful in 6m47s
🚴 Build and Test BikeControl / Android compile check (push) Successful in 3m36s
Build & Release / Build Linux (deb + AppImage) (push) Successful in 15m30s
Build & Release / Build Arch package (push) Successful in 29m17s
Build & Release / Build Android APK (push) Successful in 19m41s
Build & Release / Create release (push) Successful in 16s
Desktop icons regenerated from the new artwork with `cargo tauri icon`.
It also emits Windows, macOS and iOS variants; those are left untracked,
since bundle.icon lists only the four PNGs and this project ships deb,
AppImage, Arch and APK.

The Android set is the IconKitchen output rather than Tauri's, because
Tauri's `icon` command produces only ic_launcher and a foreground layer.
The full set adds the background and monochrome layers, which is what
makes mipmap-anydpi-v26/ic_launcher.xml a real adaptive icon: the
launcher masks it to whatever shape the device uses instead of pasting a
circle into a square, and the monochrome layer means themed icons work on
Android 13+.

These live in src-tauri/android/src/main/res/, not gen/. gen/ is rewritten
by `tauri android init`, so an icon dropped there is one git has never
seen and the next init deletes -- the same trap the Kotlin sources are
kept out of. sync-android-sources.sh already loops over res/*/ and needed
no change to pick them up. Note that check-android-sources.sh only walks
src/main/java, so res/ has no equivalent guard; the icons are tracked here
by construction rather than by a check.

AndroidManifest already pointed at @mipmap/ic_launcher and has no
roundIcon, so nothing there had to change.

Verified in the built release APK: aapt2 reports application-icon at every
density resolving to the adaptive XML, with all three layers bound, and
versionCode=1100.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:27:29 +02:00
dtourolleandClaude Opus 5 aa99b83c40 Make the Android BLE backend fail loudly and recoverably
Three defects on the path between MainActivity and the scan loop, all of
which presented as "no trainer found".

initBtleplug ran after super.onCreate, which is a race rather than a
clean ordering bug: the super chain dispatches Rust.create(), and tao's
ndk_glue spawns a thread to run `run()` on. That thread builds the
AppState and starts the scan loop concurrently. Reaching btleplug first
hits droidplug's global_adapter(), which is an `expect` — the scan task
panics and scanning is dead for the process, silently and only on some
phones. Initialising before super.onCreate means the race cannot be lost.

The same panic was reachable without any race, because init failure was
logged and shrugged off while every later call still went through to
`expect`. Failing soft is right; it just needed READY, so the call sites
can produce an ordinary "no adapter" instead of taking the task down
(NFR-4). MainActivity retries the init on resume, which is idempotent, so
a rider who launched with Bluetooth off recovers by going to Settings.

Neither of those covers a radio the rider switches off, which btleplug
does not model at all: getDefaultAdapter() returns a disabled adapter
whose scans just find nothing. MainActivity now watches
ACTION_STATE_CHANGED — the quick-settings shade never fires onResume —
and pushes the state to Rust, with requestBluetoothEnable coming back the
other way so the connection screen can offer the system dialog rather
than describing an empty room. Tri-state on purpose: unknown is not off,
or a rider with a working radio gets told to switch it on at launch.

Also: the adapter hint told Android riders to check BlueZ.

Verified on debug and release APKs for aarch64. Release matters
separately here — every one of these classes is reached only by name over
JNI, so R8 would strip or rename the lot and the failure would appear
only in a shipped build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:06:03 +02:00
dtourolleandClaude Opus 5 0679a1f524 Ride on Android: the same BLE stack, over JNI
G-4 said port to Android without rewriting the core, and nothing in
crates/core, crates/ble or crates/fit needed touching (NFR-5) — the
Android work is two files of glue and a Gradle project.

btleplug's Android backend is a hybrid crate: the GATT work happens in
Java and Rust drives it over JNI. `platform::init` has to run once with a
JNIEnv, and it cannot come from Rust's own startup — JNI resolves classes
with the calling thread's class loader, and a thread Rust spawned has
only the bootstrap loader. So MainActivity.onCreate calls into
src/android.rs, before super.onCreate: TauriActivity's super chain
synchronously starts the thread that runs `run()`, which builds AppState
and starts scanning while we are still in onCreate. Lose that race and
droidplug's global_adapter() — an `expect` — panics inside the scan task,
silently, for the life of the process.

Failing soft here is not enough for the same reason, so init sets a READY
flag and devices.rs asks before every call in. Bluetooth switched off at
launch then reads as an ordinary "no adapter", which the connection
screen already knows how to show, and onResume retries so switching it on
and coming back works.

The Java half is not a maven dependency. Upstream tells you to publish a
0.1.1-SNAPSHOT artifact to mavenLocal by hand, which no CI runner can
reproduce and which drifts from the crate silently — the failure is a
NoSuchMethodError at the first scan, not a build error. Instead
sync-android-sources.sh lifts the classes out of the btleplug and
jni-utils crate sources at exactly the versions in Cargo.lock, so a
mismatch is impossible by construction.

gen/ stays generated and untracked, so everything hand-written lives in
src-tauri/android/ and is copied back after each `tauri android init`.
check-android-sources.sh fails the build if a source exists only under
gen/ or differs from its tracked copy: both are files git has never seen
and the next init deletes, and the resulting APK builds, installs, and
behaves as though they were never written.

Permissions are split at API 31, because asking for one the platform does
not know is a permanent denial. neverForLocation on BLUETOOTH_SCAN is a
promise we can keep honestly: every scan filters by service UUID, so no
location permission is needed on Android 12+.

Also: tracing to logcat, since Android has no stdout and the default
writer drops every line into a closed fd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:52:33 +02:00