build.gradle.kts
Each line, with the release that last changed it.
| 0.5.0 | 1 | import java.util.Properties |
| 2 | ||
| 3 | plugins { | |
| 4 | alias(libs.plugins.android.application) | |
| 5 | alias(libs.plugins.kotlin.android) | |
| 6 | alias(libs.plugins.kotlin.compose) | |
| 7 | alias(libs.plugins.kotlin.serialization) | |
| 8 | alias(libs.plugins.ksp) | |
| 9 | alias(libs.plugins.hilt) | |
| 10 | } | |
| 11 | ||
| 12 | // Private, per-machine values, read from earhorn-private.properties next to this | |
| 13 | // file. That file is for your own builds only: never commit or publish it. | |
| 14 | // Public builds: run Gradle with -Pearhorn.public, which ignores the file and | |
| 15 | // leaves both values empty (those two features are then off). Keys: | |
| 16 | // earhorn.musicnowStatusUrl=http://<host>:8642/status | |
| 17 | // earhorn.domEthaDefaultUrl=http://<host>/mediacontrol.html | |
| 18 | val earhornLocal = Properties().also { props -> | |
| 19 | if (!project.hasProperty("earhorn.public")) { | |
| 20 | project.file("earhorn-private.properties").takeIf { it.isFile }?.inputStream()?.use { props.load(it) } | |
| 21 | } | |
| 22 | } | |
| 23 | fun earhornLocalString(key: String): String = | |
| 24 | "\"" + (earhornLocal.getProperty(key) ?: "").trim().replace("\\", "\\\\").replace("\"", "\\\"") + "\"" | |
| 25 | ||
| 26 | android { | |
| 27 | namespace = "dev.stave" | |
| 28 | compileSdk = libs.versions.compileSdk.get().toInt() | |
| 29 | installation { | |
| 30 | // The kukui Chromebase (MT8183, network ADB) can take minutes to | |
| 31 | // complete an install — the ddmlib default gives up long before | |
| 32 | // the ChromeOS ARC container finishes. 10 minutes of headroom. | |
| 33 | timeOutInMs = 600_000 | |
| 34 | // -r reinstall keeping data, -d allow version downgrade while | |
| 35 | // iterating between zips/builds on multiple test devices. | |
| 36 | installOptions("-r", "-d") | |
| 37 | } | |
| 38 | defaultConfig { | |
| 39 | applicationId = "dev.stave" | |
| 40 | minSdk = libs.versions.minSdk.get().toInt() | |
| 41 | targetSdk = libs.versions.targetSdk.get().toInt() | |
| 42 | // 5 = the deployed 0.2.0 build. Never reuse a versionCode: devices | |
| 43 | // refuse an update that does not increase it. | |
| 44 | versionCode = 51 | |
| 45 | versionName = "0.5.0" | |
| 46 | vectorDrawables { useSupportLibrary = true } | |
| 47 | buildConfigField("String", "MUSICNOW_STATUS_URL", earhornLocalString("earhorn.musicnowStatusUrl")) | |
| 48 | buildConfigField("String", "DOM_ETHA_DEFAULT_URL", earhornLocalString("earhorn.domEthaDefaultUrl")) | |
| 49 | } | |
| 50 | ||
| 51 | flavorDimensions += "distribution" | |
| 52 | productFlavors { | |
| 53 | create("play") { | |
| 54 | dimension = "distribution" | |
| 55 | // With Google Cast. Shipped to Play Store for devices with GPS. | |
| 56 | } | |
| 57 | create("libre") { | |
| 58 | dimension = "distribution" | |
| 59 | applicationIdSuffix = ".libre" | |
| 60 | versionNameSuffix = "-libre" | |
| 61 | // No Cast. For Fire TV, GrapheneOS, F-Droid. | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | buildTypes { | |
| 66 | debug { | |
| 67 | isMinifyEnabled = false | |
| 68 | } | |
| 69 | release { | |
| 70 | isMinifyEnabled = true | |
| 71 | // Resource shrinking is OFF — even with res/raw/keep.xml listing the | |
| 72 | // launcher icons, AGP's resource shrinker mangles manifest-only icon | |
| 73 | // references in release builds, replacing the launcher PNG with a | |
| 74 | // surviving drawable (in our case, empty_clef → APK shows clef | |
| 75 | // icon instead of the EARHORN badge). Code shrinking via R8 (minify) | |
| 76 | // remains on for size; only the resource pass is disabled. | |
| 77 | isShrinkResources = false | |
| 78 | // Sign release builds with the debug keystore so Android Studio's | |
| 79 | // run/install flow works for local testing. Without this, the | |
| 80 | // release APK comes out as `app-play-release-unsigned.apk` and | |
| 81 | // can't be installed — the installer fails (sometimes surfacing | |
| 82 | // as INSTALL_BASELINE_PROFILE_FAILED because the install never | |
| 83 | // landed and the post-install baseline-profile step then can't | |
| 84 | // find the package). | |
| 85 | // | |
| 86 | // Production / Play Store releases SHOULD override this with a | |
| 87 | // real release keystore via an environment-driven config — but | |
| 88 | // until we set that up, the debug key keeps local iteration | |
| 89 | // unblocked. The debug keystore is auto-generated by Android | |
| 90 | // Studio at ~/.android/debug.keystore on first build. | |
| 91 | signingConfig = signingConfigs.getByName("debug") | |
| 92 | proguardFiles( | |
| 93 | getDefaultProguardFile("proguard-android-optimize.txt"), | |
| 94 | "proguard-rules.pro" | |
| 95 | ) | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | compileOptions { | |
| 100 | sourceCompatibility = JavaVersion.VERSION_17 | |
| 101 | targetCompatibility = JavaVersion.VERSION_17 | |
| 102 | } | |
| 103 | ||
| 104 | kotlinOptions { | |
| 105 | jvmTarget = "17" | |
| 106 | freeCompilerArgs += listOf("-opt-in=kotlin.RequiresOptIn") | |
| 107 | } | |
| 108 | ||
| 109 | buildFeatures { | |
| 110 | compose = true | |
| 111 | // For the two private values above (see earhorn-private.properties). | |
| 112 | buildConfig = true | |
| 113 | } | |
| 114 | ||
| 115 | packaging { | |
| 116 | resources { | |
| 117 | excludes += setOf( | |
| 118 | "/META-INF/{AL2.0,LGPL2.1}", | |
| 119 | "/META-INF/DEPENDENCIES", | |
| 120 | "/META-INF/LICENSE*", | |
| 121 | "/META-INF/NOTICE*", | |
| 122 | "/META-INF/*.kotlin_module" | |
| 123 | ) | |
| 124 | pickFirsts += setOf( | |
| 125 | "/META-INF/INDEX.LIST" | |
| 126 | ) | |
| 127 | } | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | dependencies { | |
| 132 | // AndroidX core | |
| 133 | implementation(libs.androidx.core.ktx) | |
| 134 | implementation(libs.androidx.appcompat) | |
| 135 | implementation(libs.androidx.lifecycle.runtime.ktx) | |
| 136 | implementation(libs.androidx.lifecycle.viewmodel.compose) | |
| 137 | implementation(libs.androidx.activity.compose) | |
| 138 | ||
| 139 | // Compose BOM governs versions of all compose libs below | |
| 140 | implementation(platform(libs.compose.bom)) | |
| 141 | implementation(libs.compose.ui) | |
| 142 | implementation(libs.compose.ui.graphics) | |
| 143 | implementation(libs.compose.ui.tooling.preview) | |
| 144 | debugImplementation(libs.compose.ui.tooling) | |
| 145 | implementation(libs.compose.material3) | |
| 146 | implementation(libs.compose.material.icons.extended) | |
| 147 | ||
| 148 | // Compose for TV | |
| 149 | implementation(libs.androidx.tv.foundation) | |
| 150 | implementation(libs.androidx.tv.material) | |
| 151 | ||
| 152 | // Navigation + Hilt | |
| 153 | implementation(libs.androidx.navigation.compose) | |
| 154 | implementation(libs.androidx.hilt.navigation.compose) | |
| 155 | implementation(libs.hilt.android) | |
| 156 | ksp(libs.hilt.compiler) | |
| 157 | ||
| 158 | // Media3 | |
| 159 | implementation(libs.media3.exoplayer) | |
| 160 | implementation(libs.media3.session) | |
| 161 | implementation(libs.media3.ui) | |
| 162 | implementation(libs.media3.datasource.okhttp) | |
| 163 | ||
| 164 | // Room (for saved servers + preferences that need relations later) | |
| 165 | implementation(libs.room.runtime) | |
| 166 | implementation(libs.room.ktx) | |
| 167 | ksp(libs.room.compiler) | |
| 168 | ||
| 169 | // DataStore (metadata display prefs) | |
| 170 | implementation(libs.datastore.preferences) | |
| 171 | ||
| 172 | // UPnP — jUPnP 3.0.3 (maintained fork of abandoned Cling). Published to Maven Central. | |
| 173 | // jupnp.android provides AndroidUpnpServiceImpl + AndroidUpnpServiceConfiguration which wires | |
| 174 | // up the Jetty-backed transport and Recovering* XML processors jUPnP needs on Android. | |
| 175 | implementation(libs.jupnp.core) | |
| 176 | implementation(libs.jupnp.support) | |
| 177 | implementation(libs.jupnp.android) | |
| 178 | ||
| 179 | // Jetty 9.4 is jUPnP's required transport on Android. The javax.servlet dep bundled with | |
| 180 | // jetty-servlet ships an empty package that Android's runtime already partially claims; we | |
| 181 | // drop it here and let jetty pull the real javax.servlet-api through jetty-server instead. | |
| 182 | implementation(libs.jetty.server) | |
| 183 | implementation(libs.jetty.servlet) { | |
| 184 | exclude(group = "org.eclipse.jetty.orbit", module = "javax.servlet") | |
| 185 | } | |
| 186 | implementation(libs.jetty.client) | |
| 187 | ||
| 188 | // jUPnP logs via SLF4J. jdk14 binding routes to java.util.logging → logcat. | |
| 189 | implementation(libs.slf4j.api) | |
| 190 | implementation(libs.slf4j.jdk14) | |
| 191 | ||
| 192 | // Image loading for album art | |
| 193 | implementation(libs.coil.compose) | |
| 194 | implementation(libs.coil.network.okhttp) | |
| 195 | implementation(libs.coil.gif) | |
| 196 | ||
| 197 | // HTTP | |
| 198 | implementation(libs.okhttp) | |
| 199 | ||
| 200 | // Kotlinx | |
| 201 | implementation(libs.kotlinx.coroutines.android) | |
| 202 | implementation(libs.kotlinx.serialization.json) | |
| 203 | ||
| 204 | // Android Auto (cartemplate module) | |
| 205 | implementation(libs.androidx.car.app) | |
| 206 | // Legacy MediaSessionCompat — required by Car App Library | |
| 207 | // MediaPlaybackManager.registerMediaPlaybackToken(). Media3 | |
| 208 | // MediaLibrarySession.sessionCompatToken returns this type, but | |
| 209 | // the class itself lives in this support library and isn't | |
| 210 | // pulled in transitively. | |
| 211 | implementation(libs.androidx.media) | |
| 212 | ||
| 213 | // Cast — only for the play flavor | |
| 214 | "playImplementation"(libs.play.services.cast.framework) | |
| 215 | // MediaRouter — AppCompat-based chooser dialog used by the Cast sender UI. | |
| 216 | // Cast SDK normally pulls this transitively but pinning explicitly avoids | |
| 217 | // version drift and makes the dependency obvious. | |
| 218 | "playImplementation"(libs.androidx.mediarouter) | |
| 219 | } |


