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>
123 lines
4.1 KiB
Kotlin
123 lines
4.1 KiB
Kotlin
import java.util.Properties
|
|
|
|
/**
|
|
* Tracked replacement for the app module's build script.
|
|
*
|
|
* `tauri android init` regenerates gen/android from scratch, so this file is
|
|
* the source of truth and scripts/sync-android-sources.sh copies it back into
|
|
* place afterwards. It differs from Tauri's generated version in two ways:
|
|
* release signing is read from a keystore.properties written by CI, and the
|
|
* BLE Java sources synced in from the btleplug/jni-utils crates are kept out of
|
|
* R8's reach.
|
|
*/
|
|
|
|
plugins {
|
|
id("com.android.application")
|
|
id("org.jetbrains.kotlin.android")
|
|
id("rust")
|
|
}
|
|
|
|
val tauriProperties = Properties().apply {
|
|
val propFile = file("tauri.properties")
|
|
if (propFile.exists()) {
|
|
propFile.inputStream().use { load(it) }
|
|
}
|
|
}
|
|
|
|
// Release signing. CI writes gen/android/keystore.properties from secrets; when
|
|
// it is absent (any local build) the release variant simply stays debug-signed
|
|
// rather than failing the build.
|
|
val keystoreProperties = Properties().apply {
|
|
val propFile = rootProject.file("keystore.properties")
|
|
if (propFile.exists()) {
|
|
propFile.inputStream().use { load(it) }
|
|
}
|
|
}
|
|
|
|
android {
|
|
compileSdk = 36
|
|
namespace = "paris.tourolle.bikecontrol"
|
|
|
|
defaultConfig {
|
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
|
applicationId = "paris.tourolle.bikecontrol"
|
|
// 24 is Tauri's floor and also btleplug's: the jni-utils Java support
|
|
// classes use java.util.function, which arrived in API 24.
|
|
minSdk = 24
|
|
targetSdk = 36
|
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
|
}
|
|
|
|
signingConfigs {
|
|
create("release") {
|
|
keystoreProperties.getProperty("storeFile")?.let {
|
|
storeFile = file(it)
|
|
storePassword = keystoreProperties.getProperty("storePassword")
|
|
keyAlias = keystoreProperties.getProperty("keyAlias")
|
|
keyPassword = keystoreProperties.getProperty("keyPassword")
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
getByName("debug") {
|
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
|
isDebuggable = true
|
|
isJniDebuggable = true
|
|
isMinifyEnabled = false
|
|
packaging {
|
|
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
|
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
|
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
|
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
|
}
|
|
}
|
|
getByName("release") {
|
|
if (keystoreProperties.getProperty("storeFile") != null) {
|
|
signingConfig = signingConfigs.getByName("release")
|
|
}
|
|
isMinifyEnabled = true
|
|
proguardFiles(
|
|
*fileTree(".") { include("**/*.pro") }
|
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
|
.toList().toTypedArray()
|
|
)
|
|
}
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
targetCompatibility = JavaVersion.VERSION_1_8
|
|
}
|
|
|
|
kotlinOptions {
|
|
jvmTarget = "1.8"
|
|
}
|
|
|
|
buildFeatures {
|
|
buildConfig = true
|
|
}
|
|
}
|
|
|
|
rust {
|
|
rootDirRel = "../../../"
|
|
}
|
|
|
|
dependencies {
|
|
implementation("androidx.webkit:webkit:1.14.0")
|
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
|
// requestPermissions via the Activity Result API (MainActivity.kt).
|
|
implementation("androidx.activity:activity-ktx:1.10.1")
|
|
implementation("com.google.android.material:material:1.12.0")
|
|
|
|
// btleplug's Java backend and the jni-utils support classes are NOT maven
|
|
// dependencies: they are copied into app/src/main/java from the crate
|
|
// sources by scripts/sync-android-sources.sh, so the Java always matches
|
|
// the Rust in Cargo.lock. Upstream's suggested route — a
|
|
// 0.1.1-SNAPSHOT artifact published to mavenLocal by hand — cannot be
|
|
// reproduced on a CI runner and drifts silently from the crate.
|
|
}
|
|
|
|
apply(from = "tauri.build.gradle.kts")
|