DR-247. The desktop suite cannot reach ExoPlayer: it needs an Android Context and a Looper, so it exists only inside an app process. These are the same behaviours, asserted against the engine itself. Writing them forced the same gap open that mpv had. JellyTauPlayer.load(url, mediaId) had no way to express a start position, so every caller loaded and then seeked — the test could not even be written against the old signature, which is a stronger statement than a failing assertion. The position now goes to ExoPlayer with the media item via setMediaItem(item, startPositionMs), and the two-argument form delegates to it, so nothing else had to change. Running one suite against both engines settled something guesswork could not: seekWhileOpeningIsHonoured passes on ExoPlayer with no fix ExoPlayer already queues a seek issued before prepare() completes. So the lost-seek half of DR-241 was mpv-specific, and only the missing vocabulary for a start position was shared. That is the difference between "both engines have this bug" and knowing which one does. All seven cases pass on device (ROD2-W09, arm64). The fixture is a silent WAV synthesised in the cache directory at setup rather than committed or pushed: no binary in the repo, no adb step, and an exact duration, which the seek assertions depend on. Also adds the instrumentation runner to defaultConfig and teaches sync-android-sources.sh to mirror src/androidTest, the way it already mirrors src/test — so the canonical tree stays the only place tests are edited. Run: ./gradlew :app:connectedUniversalDebugAndroidTest -x :app:rustBuildUniversalDebug
156 lines
6.8 KiB
Kotlin
156 lines
6.8 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.
|
|
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()
|
|
)
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
|
|
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")
|
|
androidTestImplementation("androidx.test:runner:1.5.2")
|
|
}
|
|
|
|
apply(from = "tauri.build.gradle.kts") |