import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.hilt) } // Private, per-machine values, read from earhorn-private.properties next to this // file. That file is for your own builds only: never commit or publish it. // Public builds: run Gradle with -Pearhorn.public, which ignores the file and // leaves both values empty (those two features are then off). Keys: // earhorn.musicnowStatusUrl=http://:8642/status // earhorn.domEthaDefaultUrl=http:///mediacontrol.html val earhornLocal = Properties().also { props -> if (!project.hasProperty("earhorn.public")) { project.file("earhorn-private.properties").takeIf { it.isFile }?.inputStream()?.use { props.load(it) } } } fun earhornLocalString(key: String): String = "\"" + (earhornLocal.getProperty(key) ?: "").trim().replace("\\", "\\\\").replace("\"", "\\\"") + "\"" android { namespace = "dev.stave" compileSdk = libs.versions.compileSdk.get().toInt() installation { // The kukui Chromebase (MT8183, network ADB) can take minutes to // complete an install — the ddmlib default gives up long before // the ChromeOS ARC container finishes. 10 minutes of headroom. timeOutInMs = 600_000 // -r reinstall keeping data, -d allow version downgrade while // iterating between zips/builds on multiple test devices. installOptions("-r", "-d") } defaultConfig { applicationId = "dev.stave" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() // 5 = the deployed 0.2.0 build. Never reuse a versionCode: devices // refuse an update that does not increase it. versionCode = 51 versionName = "0.5.0" vectorDrawables { useSupportLibrary = true } buildConfigField("String", "MUSICNOW_STATUS_URL", earhornLocalString("earhorn.musicnowStatusUrl")) buildConfigField("String", "DOM_ETHA_DEFAULT_URL", earhornLocalString("earhorn.domEthaDefaultUrl")) } flavorDimensions += "distribution" productFlavors { create("play") { dimension = "distribution" // With Google Cast. Shipped to Play Store for devices with GPS. } create("libre") { dimension = "distribution" applicationIdSuffix = ".libre" versionNameSuffix = "-libre" // No Cast. For Fire TV, GrapheneOS, F-Droid. } } buildTypes { debug { isMinifyEnabled = false } release { isMinifyEnabled = true // Resource shrinking is OFF — even with res/raw/keep.xml listing the // launcher icons, AGP's resource shrinker mangles manifest-only icon // references in release builds, replacing the launcher PNG with a // surviving drawable (in our case, empty_clef → APK shows clef // icon instead of the EARHORN badge). Code shrinking via R8 (minify) // remains on for size; only the resource pass is disabled. isShrinkResources = false // Sign release builds with the debug keystore so Android Studio's // run/install flow works for local testing. Without this, the // release APK comes out as `app-play-release-unsigned.apk` and // can't be installed — the installer fails (sometimes surfacing // as INSTALL_BASELINE_PROFILE_FAILED because the install never // landed and the post-install baseline-profile step then can't // find the package). // // Production / Play Store releases SHOULD override this with a // real release keystore via an environment-driven config — but // until we set that up, the debug key keeps local iteration // unblocked. The debug keystore is auto-generated by Android // Studio at ~/.android/debug.keystore on first build. signingConfig = signingConfigs.getByName("debug") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" freeCompilerArgs += listOf("-opt-in=kotlin.RequiresOptIn") } buildFeatures { compose = true // For the two private values above (see earhorn-private.properties). buildConfig = true } packaging { resources { excludes += setOf( "/META-INF/{AL2.0,LGPL2.1}", "/META-INF/DEPENDENCIES", "/META-INF/LICENSE*", "/META-INF/NOTICE*", "/META-INF/*.kotlin_module" ) pickFirsts += setOf( "/META-INF/INDEX.LIST" ) } } } dependencies { // AndroidX core implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) // Compose BOM governs versions of all compose libs below implementation(platform(libs.compose.bom)) implementation(libs.compose.ui) implementation(libs.compose.ui.graphics) implementation(libs.compose.ui.tooling.preview) debugImplementation(libs.compose.ui.tooling) implementation(libs.compose.material3) implementation(libs.compose.material.icons.extended) // Compose for TV implementation(libs.androidx.tv.foundation) implementation(libs.androidx.tv.material) // Navigation + Hilt implementation(libs.androidx.navigation.compose) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.hilt.android) ksp(libs.hilt.compiler) // Media3 implementation(libs.media3.exoplayer) implementation(libs.media3.session) implementation(libs.media3.ui) implementation(libs.media3.datasource.okhttp) // Room (for saved servers + preferences that need relations later) implementation(libs.room.runtime) implementation(libs.room.ktx) ksp(libs.room.compiler) // DataStore (metadata display prefs) implementation(libs.datastore.preferences) // UPnP — jUPnP 3.0.3 (maintained fork of abandoned Cling). Published to Maven Central. // jupnp.android provides AndroidUpnpServiceImpl + AndroidUpnpServiceConfiguration which wires // up the Jetty-backed transport and Recovering* XML processors jUPnP needs on Android. implementation(libs.jupnp.core) implementation(libs.jupnp.support) implementation(libs.jupnp.android) // Jetty 9.4 is jUPnP's required transport on Android. The javax.servlet dep bundled with // jetty-servlet ships an empty package that Android's runtime already partially claims; we // drop it here and let jetty pull the real javax.servlet-api through jetty-server instead. implementation(libs.jetty.server) implementation(libs.jetty.servlet) { exclude(group = "org.eclipse.jetty.orbit", module = "javax.servlet") } implementation(libs.jetty.client) // jUPnP logs via SLF4J. jdk14 binding routes to java.util.logging → logcat. implementation(libs.slf4j.api) implementation(libs.slf4j.jdk14) // Image loading for album art implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) implementation(libs.coil.gif) // HTTP implementation(libs.okhttp) // Kotlinx implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) // Android Auto (cartemplate module) implementation(libs.androidx.car.app) // Legacy MediaSessionCompat — required by Car App Library // MediaPlaybackManager.registerMediaPlaybackToken(). Media3 // MediaLibrarySession.sessionCompatToken returns this type, but // the class itself lives in this support library and isn't // pulled in transitively. implementation(libs.androidx.media) // Cast — only for the play flavor "playImplementation"(libs.play.services.cast.framework) // MediaRouter — AppCompat-based chooser dialog used by the Cast sender UI. // Cast SDK normally pulls this transitively but pinning explicitly avoids // version drift and makes the dependency obvious. "playImplementation"(libs.androidx.mediarouter) }