`bun run android:dev` produced an APK whose applicationId was plain
com.dtourolle.jellytau, so installing it over a real release build failed
with INSTALL_FAILED_UPDATE_INCOMPATIBLE -- the only obvious way out being to
uninstall the release app and lose its data.
`tauri android build` rewrites the getByName("debug") block in the generated
copy of build.gradle.kts to inject its jniLibs.keepDebugSymbols entries. The
damage is visible in the generated file, where `packaging {` ends up with the
first injected line welded onto it. That rewrite drops applicationIdSuffix
and nothing else -- versionNameSuffix and the manifest placeholders beside it
survive -- and it happens after sync-android-sources.sh has copied the
canonical file into place and before Gradle configures, so no amount of
syncing beats it. The sideBySideRelease suffix in the release build type is
untouched by the same rewrite, which is why `build-and-deploy.sh release
--debug` kept working while the plain debug path did not.
The suffix moves to a top-level statement after the android {} block, which
is not inside what the rewriter looks for and survives. build-android.sh then
asserts the applicationId the APK actually carries, read from AGP's
output-metadata.json, so a future CLI that reaches further fails the build
instead of shipping a colliding APK.
Verified: a debug build now reports com.dtourolle.jellytau.debug, and the
statement is still there in gen/ after the CLI has run.
186 lines
8.6 KiB
Kotlin
186 lines
8.6 KiB
Kotlin
import java.util.Properties
|
|
|
|
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: loaded from gen/android/keystore.properties if present.
|
|
// Falls back to no signing config (debug-signed) when the file is absent.
|
|
val keystoreProperties = Properties().apply {
|
|
val propFile = rootProject.file("keystore.properties")
|
|
if (propFile.exists()) {
|
|
propFile.inputStream().use { load(it) }
|
|
}
|
|
}
|
|
|
|
// Side-by-side release: set by `scripts/build-android.sh release --debug`, which
|
|
// exports JT_SIDE_BY_SIDE=1. It puts a fully R8-minified release build into the
|
|
// debug applicationId slot, signed with the local debug keystore — so you can
|
|
// test what minification actually produces (R8 stripping JNI-loaded classes has
|
|
// broken release APKs here before) without the real signing key and without
|
|
// uninstalling your working install. Unset in CI, so distributable release
|
|
// builds are untouched.
|
|
val sideBySideRelease = System.getenv("JT_SIDE_BY_SIDE").let { it == "1" || it == "true" }
|
|
|
|
android {
|
|
compileSdk = 36
|
|
namespace = "com.dtourolle.jellytau"
|
|
defaultConfig {
|
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
|
// Launcher/app names come from placeholders so the debug build can
|
|
// rename itself without touching the generated strings.xml (a
|
|
// resValue() override there would collide with Tauri's own entries).
|
|
manifestPlaceholders["appLabel"] = "@string/app_name"
|
|
manifestPlaceholders["activityLabel"] = "@string/main_activity_title"
|
|
applicationId = "com.dtourolle.jellytau"
|
|
minSdk = 24
|
|
targetSdk = 36
|
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
|
// Required to run the on-device conformance suite
|
|
// (src/androidTest). See docs/specs/media-player-controller.md.
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
}
|
|
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") {
|
|
// Distinct applicationId so the debug build installs SIDE BY SIDE
|
|
// with a release/store install instead of demanding an uninstall
|
|
// (different signing keys on the same package = INSTALL_FAILED_
|
|
// UPDATE_INCOMPATIBLE). It gets its own data dir, its own settings
|
|
// and its own offline cache — the two are fully independent apps.
|
|
//
|
|
// This changes only the *application* id. The Kotlin/JNI classes
|
|
// stay in the `namespace` package (com.dtourolle.jellytau), so the
|
|
// fully-qualified class names Rust looks up over JNI, the manifest
|
|
// <service> entry and the R8 keep rules are all unaffected. The
|
|
// FileProvider authority is already ${applicationId}-relative.
|
|
//
|
|
// The suffix itself is applied AFTER this block -- see the bottom of
|
|
// this file. It cannot live here.
|
|
versionNameSuffix = "-debug"
|
|
manifestPlaceholders["appLabel"] = "JellyTau Debug"
|
|
manifestPlaceholders["activityLabel"] = "JellyTau 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 (sideBySideRelease) {
|
|
// Same slot, name and version scheme as the debug build type,
|
|
// plus "-release" so you can tell from Settings > Apps which of
|
|
// the two is currently sitting there. Signed with the debug
|
|
// keystore: it shares a signature with the debug build, so the
|
|
// two replace each other cleanly instead of colliding.
|
|
applicationIdSuffix = ".debug"
|
|
versionNameSuffix = "-debug-release"
|
|
manifestPlaceholders["appLabel"] = "JellyTau Debug"
|
|
manifestPlaceholders["activityLabel"] = "JellyTau Debug"
|
|
signingConfig = signingConfigs.getByName("debug")
|
|
} else if (keystoreProperties.getProperty("storeFile") != null) {
|
|
signingConfig = signingConfigs.getByName("release")
|
|
}
|
|
isMinifyEnabled = true
|
|
proguardFiles(
|
|
*fileTree(".") { include("**/*.pro") }
|
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
|
.toList().toTypedArray()
|
|
)
|
|
}
|
|
}
|
|
// Java 17 bytecode. AGP 8.11 already requires a JDK 17 toolchain to run
|
|
// (the builder image ships openjdk-17), so "1.8" was only capping the
|
|
// bytecode we emit, not the JDK in use. Kotlin's jvmTarget and javac's
|
|
// source/targetCompatibility must agree or AGP 8 fails the build, so all
|
|
// three move together.
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
kotlinOptions {
|
|
jvmTarget = "17"
|
|
}
|
|
buildFeatures {
|
|
buildConfig = true
|
|
}
|
|
}
|
|
|
|
// The debug applicationId suffix, applied from OUTSIDE the `buildTypes` block.
|
|
//
|
|
// `tauri android build` rewrites the `getByName("debug")` block in the *generated*
|
|
// copy of this file (src-tauri/gen/android/app/build.gradle.kts) to inject its
|
|
// `jniLibs.keepDebugSymbols` entries -- you can see the damage in the generated
|
|
// file, where `packaging {` ends up with the first injected line welded onto it.
|
|
// That rewrite drops `applicationIdSuffix` and nothing else: `versionNameSuffix`
|
|
// and the manifest placeholders beside it survive. It happens after
|
|
// sync-android-sources.sh has copied this file into place and before Gradle
|
|
// configures, so no amount of syncing can beat it.
|
|
//
|
|
// The result was a debug APK whose applicationId was plain
|
|
// `com.dtourolle.jellytau`, colliding with a real release install:
|
|
// INSTALL_FAILED_UPDATE_INCOMPATIBLE, with the only obvious way out being to
|
|
// uninstall the release app and lose its data. The `sideBySideRelease` suffix in
|
|
// the *release* build type is untouched by the same rewrite, which is why that
|
|
// path kept working and this one did not.
|
|
//
|
|
// A top-level statement is not inside the block the rewriter looks for, so it
|
|
// survives. scripts/build-android.sh asserts the built applicationId afterwards,
|
|
// so a future CLI that reaches further fails the build instead of shipping a
|
|
// colliding APK.
|
|
android.buildTypes.getByName("debug").applicationIdSuffix = ".debug"
|
|
|
|
rust {
|
|
rootDirRel = "../../../"
|
|
}
|
|
|
|
dependencies {
|
|
implementation("androidx.webkit:webkit:1.14.0")
|
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
|
implementation("androidx.activity:activity-ktx:1.10.1")
|
|
implementation("com.google.android.material:material:1.12.0")
|
|
|
|
// Media3 dependencies for audio playback
|
|
implementation("androidx.media3:media3-exoplayer:1.5.0")
|
|
implementation("androidx.media3:media3-exoplayer-hls:1.5.0")
|
|
implementation("androidx.media3:media3-session:1.5.0")
|
|
implementation("androidx.media3:media3-common:1.5.0")
|
|
// SubtitleView. ExoPlayer delivers cues to a listener and draws none of them
|
|
// itself: without a view to hand them to, a selected subtitle track renders
|
|
// nowhere. See JellyTauPlayer.onCues. (DR-260)
|
|
implementation("androidx.media3:media3-ui:1.5.0")
|
|
implementation("com.google.guava:guava:33.0.0-android")
|
|
|
|
// Media library for VolumeProviderCompat (remote volume control)
|
|
implementation("androidx.media:media:1.7.0")
|
|
|
|
testImplementation("junit:junit:4.13.2")
|
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
|
androidTestImplementation("androidx.test:runner:1.5.2")
|
|
}
|
|
|
|
apply(from = "tauri.build.gradle.kts") |