summaryrefslogtreecommitdiff
path: root/app/build.gradle.kts
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
committerClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
commit5876025559ba0ad287d3a7a4d9fa0246509e668e (patch)
treec918d01e2b74d205ee00fe96926232475840ff97 /app/build.gradle.kts
Scaffold Listen, with a podcast-native feed parser
Sibling to FeeDreader, not a shared module. That was a considered call: its FeedParser only inspects <enclosure> while hunting for a thumbnail, and its FilterRuleEngine is 46 lines of title substring matching that doesn't generalise to queue rules, so only Opml.kt would genuinely have transferred. The parser is the piece everything else sits on, so it goes first, test-first: 18 tests covering audio/video enclosure selection, iTunes durations in both second and clock form, season/episode numbers, podcast:chapters for the skip feature, RFC 5005 archive paging for drip subscriptions, and episode identity falling back to the enclosure URL when a guid is missing. Two things worth recording: - Built on XmlPullParserFactory rather than android.util.Xml. The latter returns null off-device, which is why FeeDreader's 297-line parser has no tests at all. Both resolve to the same KXmlParser on Android. - SimpleDateFormat with isLenient=false rejects "Tue, 02 Sep 2026 06:00:00 GMT" -- an entirely ordinary RFC 822 date. Left strict, every pubDate in the app would have been null and episode ordering would have been silently wrong everywhere. minSdk is 29, deliberately not FeeDreader's 36, which would restrict the app to Android 16 and newer. Permissions: INTERNET and ACCESS_NETWORK_STATE for feeds; FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS and WAKE_LOCK because Android requires a foreground service with a notification to play audio in the background. That notification is the transport control, not an alert -- Listen posts no new-episode notifications by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'app/build.gradle.kts')
-rw-r--r--app/build.gradle.kts140
1 files changed, 140 insertions, 0 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..a1d1854
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,140 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.plugin.serialization")
+ id("org.jetbrains.kotlin.plugin.compose")
+ id("com.google.devtools.ksp")
+ id("io.gitlab.arturbosch.detekt")
+ id("jacoco")
+}
+
+android {
+ namespace = "org.terst.listen"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "org.terst.listen"
+ // Deliberately not FeeDreader's minSdk. That value is 36 in its working tree, which
+ // would restrict the app to Android 16 and newer; 29 covers any phone in real use and
+ // is comfortably above Media3's floor of 21.
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ debug {
+ enableUnitTestCoverage = true
+ }
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+
+ testOptions {
+ unitTests {
+ // Room's and Media3's Android stubs otherwise throw on every call from a JVM test.
+ isReturnDefaultValues = true
+ }
+ }
+}
+
+dependencies {
+ val composeBom = platform("androidx.compose:compose-bom:2026.06.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation("androidx.core:core-ktx:1.17.0")
+ implementation("androidx.activity:activity-compose:1.11.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4")
+ implementation("androidx.navigation:navigation-compose:2.9.5")
+
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+
+ // Media3 is the whole playback stack: ExoPlayer for decode and speed control,
+ // MediaSessionService for lock-screen and Bluetooth transport, and its DownloadManager,
+ // which shares a cache with playback so a partly-downloaded episode streams the remainder
+ // instead of failing.
+ val media3 = "1.8.0"
+ implementation("androidx.media3:media3-exoplayer:$media3")
+ implementation("androidx.media3:media3-session:$media3")
+ implementation("androidx.media3:media3-ui:$media3")
+ implementation("androidx.media3:media3-datasource-okhttp:$media3")
+
+ implementation("androidx.room:room-runtime:2.8.4")
+ implementation("androidx.room:room-ktx:2.8.4")
+ ksp("androidx.room:room-compiler:2.8.4")
+
+ implementation("androidx.work:work-runtime-ktx:2.10.5")
+ implementation("androidx.datastore:datastore-preferences:1.1.7")
+
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
+
+ testImplementation("junit:junit:4.13.2")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
+ testImplementation("androidx.room:room-testing:2.8.4")
+ // The JVM has no XmlPullParser implementation; the feed parser needs a real one under test.
+ testImplementation("net.sf.kxml:kxml2:2.3.0")
+ testImplementation("xmlpull:xmlpull:1.1.3.1")
+ testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
+
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ androidTestImplementation("androidx.room:room-testing:2.8.4")
+ androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
+
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
+
+detekt {
+ buildUponDefaultConfig = true
+ config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
+ baseline = file("$rootDir/config/detekt/baseline.xml")
+}
+
+jacoco {
+ toolVersion = "0.8.12"
+}
+
+tasks.register<JacocoReport>("jacocoTestReport") {
+ dependsOn("testDebugUnitTest")
+ reports {
+ xml.required.set(true)
+ html.required.set(true)
+ }
+ // ui/** is excluded for the same reason as FeeDreader: Compose screens are impractical to
+ // unit-test for coverage and including them makes the metric meaningless.
+ val kotlinDebugTree = fileTree("${layout.buildDirectory.get()}/tmp/kotlin-classes/debug") {
+ exclude("**/ui/**", "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/*_Impl*")
+ }
+ val javaDebugTree = fileTree("${layout.buildDirectory.get()}/intermediates/javac/debug") {
+ exclude("**/ui/**", "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/*_Impl*")
+ }
+ classDirectories.setFrom(files(kotlinDebugTree, javaDebugTree))
+ sourceDirectories.setFrom(files("src/main/java"))
+ executionData.setFrom(fileTree(layout.buildDirectory) { include("**/*.exec") })
+}