diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c8f5f..d8d3bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,12 +65,24 @@ jobs: steps: - uses: actions/checkout@v4 - name: Assemble + unit tests + if: github.event_name == 'pull_request' run: cd android && bash ./gradlew :app:assembleDebug :app:testDebugUnitTest - uses: actions/upload-artifact@v4 + if: github.event_name == 'pull_request' with: name: app-debug.apk path: android/app/build/outputs/apk/debug/app-debug.apk if-no-files-found: error + - name: Assemble signed release + unit tests + # the release key lives on the runner host (AUDIOBRIDGE_KEYSTORE_PROPS in the runner's .env) + if: github.event_name != 'pull_request' + run: cd android && bash ./gradlew :app:assembleRelease :app:testDebugUnitTest && cp app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/AudioBridge.apk + - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' + with: + name: AudioBridge.apk + path: android/app/build/outputs/apk/release/AudioBridge.apk + if-no-files-found: error desktop-tests: # the desktop crate's tests need a real Windows host, so this one stays GitHub-hosted diff --git a/AGENTS.md b/AGENTS.md index 8ca2ac5..3fc44f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,7 @@ cargo ndk -t arm64-v8a --platform 26 clippy -p audiobridge-android --all-targets - Phone: `/audiobridge/client.key`, plus SharedPreferences `audiobridge` (`paired_pcs` JSON list of `{id, name, uri, muted}`, `mic_enabled` (also flipped remotely by a PC), `autostart_done`, `service_enabled` (the global switch, missing = on), headphone buttons: `headset_enabled`, `headset_single_earbud`, `headset_double_tap`, `headset_triple_tap`, `headset_target`). - **Global switch (phone):** `Prefs.serviceEnabled` (written with `commit`). Off stops `BridgeService` (native `setPeers("[]")` → hub stopped, no notification, no media session); every start path (`MainActivity.startBridge`, `BootReceiver`, a sticky restart in `onStartCommand`) checks `Prefs.shouldRun()`. Pairings and settings stay. It never tells PCs to switch off: they just see the phone offline. - **Default UDP port:** 47130, falling back to a random port. -- **Screenshots and releases:** the pairing QR contains the PC's secret. Never publish it; promo shots replace it with a QR of the repo URL. GitHub Releases ship `AudioBridge.exe` (the release build) and `AudioBridge.apk` (the debug-signed APK from this machine's `~/.android/debug.keystore`; a different key can't update an installed app). +- **Screenshots and releases:** the pairing QR contains the PC's secret. Never publish it; promo shots replace it with a QR of the repo URL. GitHub Releases ship `AudioBridge.exe` (the release build) and `AudioBridge.apk` (`:app:assembleRelease`, the `AudioBridge.apk` artifact of a push run on `main`). The APK is signed with the permanent release key on the netcup server: `~/.config/audiobridge/release.jks` with its passwords in `~/.config/audiobridge/keystore.properties` (both `chmod 600`, never commit them), passed to Gradle through `AUDIOBRIDGE_KEYSTORE_PROPS` (set in `~/.config/audiobridge-env.sh` and the runner's `~/runners/audiobridge/.env`); without it the release APK comes out unsigned. A different key can't update an installed app: moving from the old debug-signed APK (≤ 0.4.0) needs one uninstall + reinstall (re-pair the PCs), after that releases update in place. Losing the key means the same again for every user. ## Important Files - `Cargo.toml`: workspace deps, and profiles you must keep: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 35b6534..2dc80d9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,4 @@ +import java.util.Properties import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -6,6 +7,12 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") } +// Release signing key: AUDIOBRIDGE_KEYSTORE_PROPS names a properties file with storeFile, storePassword, +// keyAlias and keyPassword. Without it the release APK is built unsigned; debug builds are unaffected. +val releaseKeystoreProps: Properties? = providers.environmentVariable("AUDIOBRIDGE_KEYSTORE_PROPS").orNull + ?.takeIf { it.isNotBlank() } + ?.let { path -> Properties().apply { file(path).inputStream().use { load(it) } } } + android { namespace = "app.audiobridge" compileSdk = 35 @@ -21,11 +28,23 @@ android { } } + signingConfigs { + releaseKeystoreProps?.let { props -> + create("release") { + storeFile = file(props.getProperty("storeFile")) + storePassword = props.getProperty("storePassword") + keyAlias = props.getProperty("keyAlias") + keyPassword = props.getProperty("keyPassword") + } + } + } + buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + signingConfig = signingConfigs.findByName("release") } }