summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle.kts36
-rw-r--r--app/schemas/org.terst.listen.data.AppDatabase/6.json499
-rw-r--r--app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt25
-rw-r--r--app/src/main/java/org/terst/listen/data/AppDatabase.kt24
-rw-r--r--app/src/main/java/org/terst/listen/data/Entities.kt12
-rw-r--r--app/src/main/java/org/terst/listen/ui/SkipSettings.kt6
-rw-r--r--app/src/test/java/org/terst/listen/playback/PlaybackAdvancerTest.kt5
-rw-r--r--app/src/test/java/org/terst/listen/playback/PlayheadPolicyTest.kt17
8 files changed, 575 insertions, 49 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index eba61d1..bcbe4f9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -99,9 +99,11 @@ android {
buildTypes {
debug {
enableUnitTestCoverage = true
- // Produces device coverage data for jacocoFullReport. The commit gate deliberately
- // does not read it -- see that task.
- enableAndroidTestCoverage = true
+ // No enableAndroidTestCoverage. Turning it on to feed instrumented data into a
+ // coverage report instruments every class in the debug build: measured, it added
+ // 25MB to the APK (118MB -> 143MB) and runtime probes to code Peter actually uses.
+ // That is a real cost on every sideload for a report that would be run rarely, so
+ // the framework-adapter exclusions carry this on their own.
}
release {
isMinifyEnabled = false
@@ -297,31 +299,3 @@ tasks.register<JacocoReport>("jacocoTestReport") {
sourceDirectories.setFrom(files("src/main/java"))
executionData.setFrom(fileTree(layout.buildDirectory) { include("**/*.exec") })
}
-
-/**
- * Unit *and* instrumented coverage, for when you want the fuller picture.
- *
- * Deliberately not what the commit gate reads. Instrumented data only exists after a device run,
- * so folding it into the gate would make the number depend on whether one happened recently and
- * on stale .ec files lying around -- a gate that reports different figures for identical code is
- * worse than a narrow one. Run this with a device attached:
- *
- * ./gradlew connectedDebugAndroidTest jacocoFullReport
- */
-tasks.register<JacocoReport>("jacocoFullReport") {
- dependsOn("testDebugUnitTest")
- reports {
- xml.required.set(true)
- html.required.set(true)
- }
- val kotlinDebugTree = fileTree("${layout.buildDirectory.get()}/tmp/kotlin-classes/debug") {
- exclude(coverageExclusions)
- }
- val javaDebugTree = fileTree("${layout.buildDirectory.get()}/intermediates/javac/debug") {
- exclude(coverageExclusions)
- }
- classDirectories.setFrom(files(kotlinDebugTree, javaDebugTree))
- sourceDirectories.setFrom(files("src/main/java"))
- // .ec is what a device run writes, alongside the JVM run's .exec.
- executionData.setFrom(fileTree(layout.buildDirectory) { include("**/*.exec", "**/*.ec") })
-}
diff --git a/app/schemas/org.terst.listen.data.AppDatabase/6.json b/app/schemas/org.terst.listen.data.AppDatabase/6.json
new file mode 100644
index 0000000..1820193
--- /dev/null
+++ b/app/schemas/org.terst.listen.data.AppDatabase/6.json
@@ -0,0 +1,499 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 6,
+ "identityHash": "4a70b3102d3822eabaf209d5a446fc40",
+ "entities": [
+ {
+ "tableName": "subscriptions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `feedUrl` TEXT NOT NULL, `kind` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT, `author` TEXT, `imageUrl` TEXT, `link` TEXT, `addedAtMs` INTEGER NOT NULL, `etag` TEXT, `lastModified` TEXT, `lastFetchedAtMs` INTEGER, `lastError` TEXT, `playbackSpeed` REAL, `skipIntroMs` INTEGER NOT NULL, `skipOutroMs` INTEGER NOT NULL, `dripEnabled` INTEGER NOT NULL DEFAULT 0, `dripIntervalMs` INTEGER NOT NULL DEFAULT 86400000, `dripCount` INTEGER NOT NULL DEFAULT 1, `dripOldestFirst` INTEGER NOT NULL DEFAULT 1, `dripLastReleaseAtMs` INTEGER, `skipAdChapters` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "feedUrl",
+ "columnName": "feedUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "kind",
+ "columnName": "kind",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "link",
+ "columnName": "link",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "addedAtMs",
+ "columnName": "addedAtMs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "etag",
+ "columnName": "etag",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "lastModified",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastFetchedAtMs",
+ "columnName": "lastFetchedAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "lastError",
+ "columnName": "lastError",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "playbackSpeed",
+ "columnName": "playbackSpeed",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "skipIntroMs",
+ "columnName": "skipIntroMs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "skipOutroMs",
+ "columnName": "skipOutroMs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dripEnabled",
+ "columnName": "dripEnabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "dripIntervalMs",
+ "columnName": "dripIntervalMs",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "86400000"
+ },
+ {
+ "fieldPath": "dripCount",
+ "columnName": "dripCount",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "dripOldestFirst",
+ "columnName": "dripOldestFirst",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "dripLastReleaseAtMs",
+ "columnName": "dripLastReleaseAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "skipAdChapters",
+ "columnName": "skipAdChapters",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_subscriptions_feedUrl",
+ "unique": true,
+ "columnNames": [
+ "feedUrl"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_feedUrl` ON `${TABLE_NAME}` (`feedUrl`)"
+ }
+ ]
+ },
+ {
+ "tableName": "episodes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `guid` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT, `mediaUrl` TEXT NOT NULL, `mediaType` TEXT NOT NULL, `mediaLengthBytes` INTEGER, `durationMs` INTEGER, `publishedAtMs` INTEGER, `seasonNumber` INTEGER, `episodeNumber` INTEGER, `imageUrl` TEXT, `chaptersUrl` TEXT, `discoveredAtMs` INTEGER NOT NULL, `positionMs` INTEGER NOT NULL, `playedAtMs` INTEGER, `downloadState` TEXT NOT NULL, `localPath` TEXT, `downloadedAtMs` INTEGER, `dripReleasedAtMs` INTEGER, `appliedRuleId` TEXT, `appliedRuleReason` TEXT, `speedOverride` REAL, `chaptersJson` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`subscriptionId`) REFERENCES `subscriptions`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscriptionId",
+ "columnName": "subscriptionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "guid",
+ "columnName": "guid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "mediaUrl",
+ "columnName": "mediaUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaType",
+ "columnName": "mediaType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mediaLengthBytes",
+ "columnName": "mediaLengthBytes",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "durationMs",
+ "columnName": "durationMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "publishedAtMs",
+ "columnName": "publishedAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "seasonNumber",
+ "columnName": "seasonNumber",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "episodeNumber",
+ "columnName": "episodeNumber",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chaptersUrl",
+ "columnName": "chaptersUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "discoveredAtMs",
+ "columnName": "discoveredAtMs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "positionMs",
+ "columnName": "positionMs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "playedAtMs",
+ "columnName": "playedAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "downloadState",
+ "columnName": "downloadState",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "localPath",
+ "columnName": "localPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "downloadedAtMs",
+ "columnName": "downloadedAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "dripReleasedAtMs",
+ "columnName": "dripReleasedAtMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "appliedRuleId",
+ "columnName": "appliedRuleId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "appliedRuleReason",
+ "columnName": "appliedRuleReason",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "speedOverride",
+ "columnName": "speedOverride",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "chaptersJson",
+ "columnName": "chaptersJson",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_episodes_subscriptionId",
+ "unique": false,
+ "columnNames": [
+ "subscriptionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_subscriptionId` ON `${TABLE_NAME}` (`subscriptionId`)"
+ },
+ {
+ "name": "index_episodes_subscriptionId_guid",
+ "unique": true,
+ "columnNames": [
+ "subscriptionId",
+ "guid"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_episodes_subscriptionId_guid` ON `${TABLE_NAME}` (`subscriptionId`, `guid`)"
+ },
+ {
+ "name": "index_episodes_publishedAtMs",
+ "unique": false,
+ "columnNames": [
+ "publishedAtMs"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_publishedAtMs` ON `${TABLE_NAME}` (`publishedAtMs`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "subscriptions",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "subscriptionId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "queue",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`episodeId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`episodeId`), FOREIGN KEY(`episodeId`) REFERENCES `episodes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "episodeId",
+ "columnName": "episodeId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "episodeId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_queue_position",
+ "unique": false,
+ "columnNames": [
+ "position"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_queue_position` ON `${TABLE_NAME}` (`position`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "episodes",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "episodeId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `subscriptionId` TEXT, `titleContains` TEXT, `titleExcludes` TEXT, `minDurationMs` INTEGER, `maxDurationMs` INTEGER, `maxAgeMs` INTEGER, `seasonNumber` INTEGER, `action` TEXT NOT NULL, `autoDownload` INTEGER NOT NULL, `speed` REAL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscriptionId",
+ "columnName": "subscriptionId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "titleContains",
+ "columnName": "titleContains",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "titleExcludes",
+ "columnName": "titleExcludes",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "minDurationMs",
+ "columnName": "minDurationMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "maxDurationMs",
+ "columnName": "maxDurationMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "maxAgeMs",
+ "columnName": "maxAgeMs",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "seasonNumber",
+ "columnName": "seasonNumber",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "action",
+ "columnName": "action",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoDownload",
+ "columnName": "autoDownload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "speed",
+ "columnName": "speed",
+ "affinity": "REAL"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_rules_position",
+ "unique": false,
+ "columnNames": [
+ "position"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_rules_position` ON `${TABLE_NAME}` (`position`)"
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a70b3102d3822eabaf209d5a446fc40')"
+ ]
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt b/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt
index 181e853..cf691e0 100644
--- a/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt
+++ b/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt
@@ -197,4 +197,29 @@ class MigrationTest {
assertEquals(2, cursor.getInt(0))
}
}
+
+ /**
+ * Ad-chapter skipping was shipped off and Peter asked for it on after living with it, so
+ * existing shows have to move rather than only new ones picking up the new default.
+ */
+ @Test
+ fun migrates5To6TurningAdChapterSkippingOn() {
+ helper.createDatabase(name, 5).use { db ->
+ db.execSQL(
+ """INSERT INTO subscriptions
+ (id, feedUrl, kind, title, addedAtMs, skipIntroMs, skipOutroMs, dripEnabled,
+ dripIntervalMs, dripCount, dripOldestFirst, skipAdChapters)
+ VALUES ('s1', 'https://example.com/feed.xml', 'PODCAST', 'A Show', 1000, 0, 0,
+ 0, 86400000, 1, 1, 0)"""
+ )
+ }
+
+ val db = helper.runMigrationsAndValidate(name, 6, true, AppDatabase.MIGRATION_5_6)
+
+ db.query("SELECT skipAdChapters, title FROM subscriptions WHERE id = 's1'").use { cursor ->
+ cursor.moveToFirst()
+ assertEquals(1, cursor.getInt(0))
+ assertEquals("the show itself must survive the flip", "A Show", cursor.getString(1))
+ }
+ }
}
diff --git a/app/src/main/java/org/terst/listen/data/AppDatabase.kt b/app/src/main/java/org/terst/listen/data/AppDatabase.kt
index fe73ce0..4ff9f50 100644
--- a/app/src/main/java/org/terst/listen/data/AppDatabase.kt
+++ b/app/src/main/java/org/terst/listen/data/AppDatabase.kt
@@ -25,7 +25,7 @@ class Converters {
QueueEntity::class,
RuleEntity::class,
],
- version = 5,
+ version = 6,
exportSchema = true,
)
@TypeConverters(Converters::class)
@@ -141,6 +141,24 @@ abstract class AppDatabase : RoomDatabase() {
override fun onCreate(db: SupportSQLiteDatabase) = seedDefaultFilters(db)
}
+ /**
+ * Turns ad-chapter skipping on, which Peter asked for after living with it.
+ *
+ * It shipped off-by-default out of caution, since it is the one part of the skip
+ * feature that removes audio nobody has heard. In practice it only ever acts on
+ * chapters a publisher explicitly marked as not-content, and doing that automatically
+ * is the point.
+ *
+ * Only the stored rows move. The column's SQL default stays 0 because nothing inserts
+ * a subscription without it -- Room always writes every column -- so changing it would
+ * mean recreating the table for a value that is never read.
+ */
+ val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("UPDATE subscriptions SET skipAdChapters = 1")
+ }
+ }
+
fun get(context: Context): AppDatabase = instance ?: synchronized(this) {
instance ?: build(context.applicationContext).also { instance = it }
}
@@ -149,7 +167,9 @@ abstract class AppDatabase : RoomDatabase() {
Room.databaseBuilder(context, AppDatabase::class.java, "listen.db")
// Listed one by one rather than spread from an array: every migration here is a
// deliberate addition, and the list is the record of them.
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
+ .addMigrations(
+ MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6,
+ )
.addCallback(SEED_DEFAULT_FILTERS)
// No fallbackToDestructiveMigration. A listener's playback positions and play
// history are not reproducible from anywhere, so a missing migration must fail
diff --git a/app/src/main/java/org/terst/listen/data/Entities.kt b/app/src/main/java/org/terst/listen/data/Entities.kt
index ed80c4b..547a4ad 100644
--- a/app/src/main/java/org/terst/listen/data/Entities.kt
+++ b/app/src/main/java/org/terst/listen/data/Entities.kt
@@ -88,11 +88,15 @@ data class SubscriptionEntity(
/**
* Whether to pass over chapters the publisher marked `toc: false`.
*
- * Off by default and opt-in per show. Acting on someone else's metadata to remove audio a
- * listener has not heard is the one part of this feature that can eat an episode, so it is
- * never on unless asked for.
+ * On by default, at Peter's request after living with it off. Only ever acts on sections a
+ * publisher explicitly marked as not part of the episode -- never guessed from a title --
+ * and doing that without being asked each time is the point of the feature.
+ *
+ * The SQL default stays 0 while the Kotlin default is true: Room writes every column on
+ * insert, so the SQL one is only consulted by an ALTER TABLE that has already happened.
+ * Changing it would mean recreating the table for a value nothing reads.
*/
- @ColumnInfo(defaultValue = "0") val skipAdChapters: Boolean = false,
+ @ColumnInfo(defaultValue = "0") val skipAdChapters: Boolean = true,
)
/** One a day, the cadence a drip subscription is for. */
diff --git a/app/src/main/java/org/terst/listen/ui/SkipSettings.kt b/app/src/main/java/org/terst/listen/ui/SkipSettings.kt
index 7c6146c..106e3fd 100644
--- a/app/src/main/java/org/terst/listen/ui/SkipSettings.kt
+++ b/app/src/main/java/org/terst/listen/ui/SkipSettings.kt
@@ -66,9 +66,9 @@ fun SkipSettings(
Text("Skip marked breaks", Modifier.padding(start = 12.dp))
}
Text(
- "Only where the show publishes chapters and marks a section as not part of the " +
- "episode. Nothing is guessed from chapter titles, so a show discussing its own " +
- "sponsors keeps playing.",
+ "On by default. Only acts where the show publishes chapters and marks a section " +
+ "as not part of the episode -- nothing is guessed from chapter titles, so a " +
+ "show discussing its own sponsors keeps playing.",
style = MaterialTheme.typography.bodySmall,
)
}
diff --git a/app/src/test/java/org/terst/listen/playback/PlaybackAdvancerTest.kt b/app/src/test/java/org/terst/listen/playback/PlaybackAdvancerTest.kt
index e22be1e..171114d 100644
--- a/app/src/test/java/org/terst/listen/playback/PlaybackAdvancerTest.kt
+++ b/app/src/test/java/org/terst/listen/playback/PlaybackAdvancerTest.kt
@@ -49,6 +49,7 @@ class PlaybackAdvancerTest {
skipIntroMs: Long = 0,
playbackSpeed: Float? = null,
kind: SubscriptionKind = SubscriptionKind.PODCAST,
+ skipAdChapters: Boolean = true,
): SubscriptionEntity {
val entity = SubscriptionEntity(
id = id,
@@ -58,6 +59,7 @@ class PlaybackAdvancerTest {
addedAtMs = 0,
skipIntroMs = skipIntroMs,
playbackSpeed = playbackSpeed,
+ skipAdChapters = skipAdChapters,
)
subscriptions.insert(entity)
return entity
@@ -356,7 +358,8 @@ class PlaybackAdvancerTest {
),
) { now }
- show(id = "s1")
+ // Explicitly off, rather than leaning on a default that has since flipped to on.
+ show(id = "s1", skipAdChapters = false)
episode("a")
assertEquals(emptyList<Chapter>(), fetching.contextFor("a").chapters)
assertEquals(0, asked)
diff --git a/app/src/test/java/org/terst/listen/playback/PlayheadPolicyTest.kt b/app/src/test/java/org/terst/listen/playback/PlayheadPolicyTest.kt
index 1488559..d0a49f7 100644
--- a/app/src/test/java/org/terst/listen/playback/PlayheadPolicyTest.kt
+++ b/app/src/test/java/org/terst/listen/playback/PlayheadPolicyTest.kt
@@ -69,22 +69,23 @@ class PlayheadPolicyTest {
}
@Test
- fun `inside a marked break, on a show that opted in, it seeks past it`() {
+ fun `inside a marked break it seeks past it`() {
assertEquals(
TickAction.SeekTo(3 * minute),
- decide(
- show = show.copy(skipAdChapters = true),
- chapters = chapters,
- positionMs = 2 * minute + 5_000,
- ),
+ decide(show = show, chapters = chapters, positionMs = 2 * minute + 5_000),
)
}
+ /** Opted out explicitly: skipping is on by default, so this is the deliberate case. */
@Test
- fun `a show that did not opt in keeps playing the break`() {
+ fun `a show that opted out keeps playing the break`() {
assertEquals(
TickAction.Nothing,
- decide(show = show, chapters = chapters, positionMs = 2 * minute + 5_000),
+ decide(
+ show = show.copy(skipAdChapters = false),
+ chapters = chapters,
+ positionMs = 2 * minute + 5_000,
+ ),
)
}