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>
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fail if any hand-written Android source has ended up only in gen/android.
|
||||
#
|
||||
# src-tauri/gen/ is not tracked and `tauri android init` rewrites it, so a
|
||||
# Kotlin file edited *there* is a file that will be silently deleted by the next
|
||||
# init and has never existed in git. That failure is quiet in exactly the wrong
|
||||
# way: the build keeps working locally for as long as nobody re-inits, and then
|
||||
# a CI release APK ships without whatever the file did.
|
||||
#
|
||||
# Every source under gen/android/app/src/main/java must therefore be one of:
|
||||
#
|
||||
# 1. Tauri's own, under <pkg>/generated/ and carrying the AUTO-GENERATED
|
||||
# banner. Regenerated on every init; not ours to keep.
|
||||
# 2. A copy of a tracked file in src-tauri/android/, byte for byte. If it
|
||||
# differs, the edit was made in gen/ and is about to be lost.
|
||||
# 3. Vendored from a crate by sync-android-sources.sh — the btleplug and
|
||||
# jni-utils Java backends.
|
||||
#
|
||||
# Anything else is hand-written and untracked, and this exits non-zero.
|
||||
#
|
||||
# scripts/check-android-sources.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TRACKED="$ROOT/src-tauri/android/src/main/java"
|
||||
GEN="$ROOT/src-tauri/gen/android/app/src/main/java"
|
||||
|
||||
if [ ! -d "$GEN" ]; then
|
||||
echo "ℹ️ No generated Android project — nothing to check."
|
||||
echo " (run 'cargo tauri android init' then scripts/sync-android-sources.sh)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fail=0
|
||||
drift=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
rel="${file#"$GEN"/}"
|
||||
|
||||
case "$rel" in
|
||||
# 3. Vendored from the crates.
|
||||
com/nonpolynomial/btleplug/*|io/github/gedgygedgy/rust/*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
# 1. Tauri's own.
|
||||
if [ "${rel#*/generated/}" != "$rel" ] && head -8 "$file" | grep -q 'AUTO-GENERATED'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# 2. Ours — must exist in src-tauri/android and match.
|
||||
if [ -f "$TRACKED/$rel" ]; then
|
||||
if ! cmp -s "$TRACKED/$rel" "$file"; then
|
||||
echo "❌ $rel differs from the tracked copy."
|
||||
echo " Edited in gen/ — the next 'tauri android init' will delete it."
|
||||
echo " Move the change into src-tauri/android/src/main/java/$rel"
|
||||
drift=1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "❌ $rel is hand-written and untracked."
|
||||
echo " Move it to src-tauri/android/src/main/java/$rel and add it to the"
|
||||
echo " copy list in scripts/sync-android-sources.sh."
|
||||
fail=1
|
||||
done < <(find "$GEN" -type f \( -name '*.kt' -o -name '*.java' \))
|
||||
|
||||
# The other direction: a tracked source the sync script never copies is dead
|
||||
# code that looks live.
|
||||
while IFS= read -r file; do
|
||||
rel="${file#"$TRACKED"/}"
|
||||
if [ ! -f "$GEN/$rel" ]; then
|
||||
echo "❌ src-tauri/android/src/main/java/$rel is tracked but never reached gen/."
|
||||
echo " Add it to scripts/sync-android-sources.sh, or delete it."
|
||||
fail=1
|
||||
fi
|
||||
done < <(find "$TRACKED" -type f \( -name '*.kt' -o -name '*.java' \) 2>/dev/null || true)
|
||||
|
||||
if [ "$fail" = 1 ] || [ "$drift" = 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Every hand-written Android source is tracked in src-tauri/android/"
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# Populate src-tauri/gen/android with the parts `tauri android init` cannot know
|
||||
# about.
|
||||
#
|
||||
# `tauri android init` regenerates gen/android from tauri.conf.json, and gen/ is
|
||||
# not tracked (see .gitignore). Anything hand-maintained therefore lives under
|
||||
# src-tauri/android/ and is copied in by this script after every init. Run it
|
||||
# between `tauri android init` and `tauri android build`.
|
||||
#
|
||||
# Two kinds of thing are copied:
|
||||
#
|
||||
# 1. Our own files — AndroidManifest.xml (BLE permissions), MainActivity.kt,
|
||||
# the app build.gradle.kts (signing config + BLE Java sources), ProGuard
|
||||
# keep rules.
|
||||
#
|
||||
# 2. btleplug's Android backend, which is a *hybrid* Rust/Java crate: the Rust
|
||||
# side registers native methods on Java classes that must be compiled into
|
||||
# the APK. Upstream tells you to publish a SNAPSHOT maven artifact; we
|
||||
# instead lift the Java straight out of the crate sources that Cargo has
|
||||
# already downloaded, keyed on the exact versions in Cargo.lock. That makes
|
||||
# a Java/Rust version mismatch — the failure mode here is a
|
||||
# NoSuchMethodError at first scan, not a build error — impossible by
|
||||
# construction.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SRC="$ROOT/src-tauri/android"
|
||||
GEN="$ROOT/src-tauri/gen/android"
|
||||
APP="$GEN/app/src/main"
|
||||
PKG_PATH="paris/tourolle/bikecontrol"
|
||||
|
||||
if [ ! -d "$GEN" ]; then
|
||||
echo "❌ $GEN does not exist — run 'cargo tauri android init' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Syncing Android sources into gen/android…"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Our own files.
|
||||
|
||||
mkdir -p "$APP/java/$PKG_PATH"
|
||||
cp "$SRC/src/main/java/$PKG_PATH/MainActivity.kt" "$APP/java/$PKG_PATH/MainActivity.kt"
|
||||
echo " ✓ MainActivity.kt"
|
||||
|
||||
# Gradle reads ONLY the gen/ copy — there is no manifest-merger hook for our
|
||||
# entries — so this tracked file must be the complete manifest.
|
||||
cp "$SRC/src/main/AndroidManifest.xml" "$APP/AndroidManifest.xml"
|
||||
echo " ✓ AndroidManifest.xml"
|
||||
|
||||
cp "$SRC/app/build.gradle.kts" "$GEN/app/build.gradle.kts"
|
||||
echo " ✓ app/build.gradle.kts"
|
||||
|
||||
# build.gradle.kts globs **/*.pro, so dropping this in app/ is enough.
|
||||
cp "$SRC/app/proguard-bikecontrol.pro" "$GEN/app/proguard-bikecontrol.pro"
|
||||
echo " ✓ proguard-bikecontrol.pro"
|
||||
|
||||
if [ -d "$SRC/src/main/res" ]; then
|
||||
for dir in "$SRC/src/main/res"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
name="$(basename "$dir")"
|
||||
mkdir -p "$APP/res/$name"
|
||||
cp "$dir"/* "$APP/res/$name/"
|
||||
echo " ✓ res/$name"
|
||||
done
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. btleplug's Java backend, at the versions Cargo.lock pins.
|
||||
|
||||
crate_version() {
|
||||
# First `version = "x"` line after the crate's `name =` line in Cargo.lock.
|
||||
awk -v pkg="name = \"$1\"" '
|
||||
$0 == pkg { found = 1; next }
|
||||
found && /^version = / { gsub(/[",]/, "", $3); print $3; exit }
|
||||
' "$ROOT/Cargo.lock"
|
||||
}
|
||||
|
||||
crate_src() {
|
||||
local name="$1" version="$2" dir
|
||||
for dir in "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/"$name-$version"; do
|
||||
[ -d "$dir" ] && { printf '%s' "$dir"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
BTLEPLUG_VERSION="$(crate_version btleplug)"
|
||||
JNI_UTILS_VERSION="$(crate_version jni-utils)"
|
||||
|
||||
if [ -z "$BTLEPLUG_VERSION" ] || [ -z "$JNI_UTILS_VERSION" ]; then
|
||||
echo "❌ Could not read btleplug/jni-utils versions from Cargo.lock." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# jni-utils is an Android-only dependency of btleplug, so a plain `cargo fetch`
|
||||
# for the host will not have downloaded it.
|
||||
if ! crate_src jni-utils "$JNI_UTILS_VERSION" >/dev/null; then
|
||||
echo " ↓ fetching Android-target crate sources"
|
||||
(cd "$ROOT" && cargo fetch --target aarch64-linux-android >/dev/null)
|
||||
fi
|
||||
|
||||
BTLEPLUG_SRC="$(crate_src btleplug "$BTLEPLUG_VERSION")" || {
|
||||
echo "❌ btleplug $BTLEPLUG_VERSION sources not found in the cargo registry." >&2
|
||||
exit 1
|
||||
}
|
||||
JNI_UTILS_SRC="$(crate_src jni-utils "$JNI_UTILS_VERSION")" || {
|
||||
echo "❌ jni-utils $JNI_UTILS_VERSION sources not found in the cargo registry." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Drop any previous copy first: a class left behind from an older crate version
|
||||
# would still compile and would still be found at runtime.
|
||||
rm -rf "$APP/java/com/nonpolynomial" "$APP/java/io/github/gedgygedgy"
|
||||
mkdir -p "$APP/java"
|
||||
|
||||
cp -r "$BTLEPLUG_SRC/src/droidplug/java/src/main/java/com" "$APP/java/"
|
||||
echo " ✓ btleplug $BTLEPLUG_VERSION Java backend (com.nonpolynomial.btleplug)"
|
||||
|
||||
cp -r "$JNI_UTILS_SRC/java/src/main/java/io" "$APP/java/"
|
||||
echo " ✓ jni-utils $JNI_UTILS_VERSION Java support (io.github.gedgygedgy.rust)"
|
||||
|
||||
echo "✅ Android sources synced"
|
||||
Reference in New Issue
Block a user