1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
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")
id("com.chaquo.python")
}
android {
// Room schema JSON, so migrations can be tested against a real prior schema.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
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"
ndk {
// CPython ships as native code, so every ABI included multiplies the APK size.
// arm64 is the phone; x86_64 is the emulator.
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
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")
testImplementation("androidx.test:core:1.7.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")
}
chaquopy {
defaultConfig {
// Not the default 3.8. yt-dlp dropped Python 3.8 support, so pip silently resolves the
// last release that still supported it -- 2024.10.22 -- which is stale enough that
// YouTube extraction no longer works. The build succeeds and the feature doesn't.
version = "3.12"
// The extractor itself. yt-dlp is pure Python and updates independently of this app,
// which is the entire reason for embedding it rather than reimplementing YouTube's
// stream resolution against a target that changes without notice.
pip {
install("yt-dlp")
}
}
}
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") })
}
|