R8 has broken release APKs here before by stripping the JNI-loaded player and security classes, and the only way to reproduce that was to build with the real signing key and clobber the install you actually use. `./scripts/build-and-deploy.sh release --device --debug` now builds a fully minified release APK — exactly what ships — into the .debug applicationId slot, signed with the local debug keystore: release com.dtourolle.jellytau 0.5.5 release --debug com.dtourolle.jellytau.debug 0.5.5-debug-release debug com.dtourolle.jellytau.debug 0.5.5-debug It shares the applicationId *and* the signature with the plain debug build, so the two replace each other cleanly rather than colliding, and the versionName suffix says which is currently installed. No real key is needed, so the side-by-side path deliberately skips write-keystore-properties.sh. The flag reaches Gradle as JT_SIDE_BY_SIDE=1. CI never sets it, and the release manifest merges byte-identical without it — verified both ways through processUniversalReleaseMainManifest. deploy-android.sh and build-and-deploy.sh learned the flag too, since the APK path is unchanged but the package to launch is not.
143 lines
6.1 KiB
Kotlin
143 lines
6.1 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")
|
|
}
|
|
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.
|
|
applicationIdSuffix = ".debug"
|
|
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()
|
|
)
|
|
}
|
|
}
|
|
kotlinOptions {
|
|
jvmTarget = "1.8"
|
|
}
|
|
buildFeatures {
|
|
buildConfig = true
|
|
}
|
|
}
|
|
|
|
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")
|
|
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")
|
|
}
|
|
|
|
apply(from = "tauri.build.gradle.kts") |