summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-26 09:25:00 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-09-26 09:25:00 +0000
commit12a0f7f07179f4d2b98a5db3c2a6bca33b57644c (patch)
treeb34eb0933a59a5e9c9eb940e3dbdfa4fa102f5f4 /app/src
parent750f4a54cbce570c3d555bceadab68d1b25b47e7 (diff)
Seed the default filters on a fresh install too, not only on upgrade
Caught while verifying on a device: after a reinstall the rules table was empty. A fresh install creates the schema at the current version and never runs a migration, so putting the seeding only in MIGRATION_4_5 gave the trailer and preview filters to upgraders and silently to nobody else. Peter's own phone would have been fine -- it is an upgrade from v4 -- but a reinstall later would have quietly lost them, which is the kind of gap that surfaces as "I thought you set that up". The SQL is now shared between the migration and a RoomDatabase.Callback, and covered by an instrumented test building a fresh database and asserting both filters arrive. No permission changes, no schema change. 425 unit tests green, 11 instrumented, detekt and lint clean. Coverage 65.18 -> 65.04, exception recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'app/src')
-rw-r--r--app/src/androidTest/java/org/terst/listen/data/LibraryOrderTest.kt23
-rw-r--r--app/src/main/java/org/terst/listen/data/AppDatabase.kt58
2 files changed, 56 insertions, 25 deletions
diff --git a/app/src/androidTest/java/org/terst/listen/data/LibraryOrderTest.kt b/app/src/androidTest/java/org/terst/listen/data/LibraryOrderTest.kt
index a181dd7..4117b79 100644
--- a/app/src/androidTest/java/org/terst/listen/data/LibraryOrderTest.kt
+++ b/app/src/androidTest/java/org/terst/listen/data/LibraryOrderTest.kt
@@ -119,6 +119,29 @@ class LibraryOrderTest {
assertEquals(listOf("b", "a"), order())
}
+ /**
+ * A fresh install gets the default filters too.
+ *
+ * Found on a device: a reinstall creates the schema at the current version and never runs a
+ * migration, so seeding only in MIGRATION_4_5 quietly gave them to upgraders alone.
+ */
+ @Test
+ fun aFreshDatabaseIsSeededWithTheDefaultFilters() = runBlocking {
+ val fresh = Room.inMemoryDatabaseBuilder(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ AppDatabase::class.java,
+ ).addCallback(AppDatabase.SEED_DEFAULT_FILTERS).build()
+
+ try {
+ // Room creates the database lazily, so the callback has not run until it is used.
+ val rules = fresh.rules().getAll()
+ assertEquals(listOf("trailer", "preview"), rules.map { it.titleContains })
+ assertEquals(listOf(RuleAction.MARK_PLAYED, RuleAction.MARK_PLAYED), rules.map { it.action })
+ } finally {
+ fresh.close()
+ }
+ }
+
/** A show with nothing waiting sorts last, by title, rather than vanishing. */
@Test
fun aShowWithNothingWaitingSortsLast() = runBlocking {
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 c33921b..fe73ce0 100644
--- a/app/src/main/java/org/terst/listen/data/AppDatabase.kt
+++ b/app/src/main/java/org/terst/listen/data/AppDatabase.kt
@@ -101,37 +101,44 @@ abstract class AppDatabase : RoomDatabase() {
/**
* Seeds the trailer and preview filters Peter asked for.
*
- * Rules live in the database on the phone, so a migration is the only way to put them
- * there. They are ordinary rules: visible in the Rules screen, and editable, disableable
- * or deletable like any other. Appended after whatever is already there rather than
+ * Rules live in the database on the phone, so code is the only way to put them there.
+ * They are ordinary rules: visible in the Rules screen, and editable, disableable or
+ * deletable like any other. Appended after whatever is already there rather than
* taking position 0, so they cannot pre-empt rules already being relied on, and they
* only act on newly discovered episodes -- nothing already in the library changes.
*
- * INSERT OR IGNORE on a fixed id, so deleting one does not bring it back on the next
- * upgrade.
+ * INSERT OR IGNORE on a fixed id, so deleting one does not bring it back.
*/
+ private fun seedDefaultFilters(db: SupportSQLiteDatabase) {
+ seedFilter(db, "listen-default-trailer", "Trailers", "trailer")
+ seedFilter(db, "listen-default-preview", "Previews", "preview")
+ }
+
+ private fun seedFilter(db: SupportSQLiteDatabase, id: String, name: String, word: String) {
+ db.execSQL(
+ "INSERT OR IGNORE INTO rules " +
+ "(id, name, position, enabled, subscriptionId, titleContains, " +
+ "titleExcludes, minDurationMs, maxDurationMs, maxAgeMs, seasonNumber, " +
+ "action, autoDownload, speed) " +
+ "SELECT ?, ?, COALESCE(MAX(position), -1) + 1, 1, NULL, ?, NULL, " +
+ "NULL, NULL, NULL, NULL, 'MARK_PLAYED', 0, NULL FROM rules",
+ arrayOf(id, name, word),
+ )
+ }
+
val MIGRATION_4_5 = object : Migration(4, 5) {
- override fun migrate(db: SupportSQLiteDatabase) {
- seedFilter(db, "listen-default-trailer", "Trailers", "trailer")
- seedFilter(db, "listen-default-preview", "Previews", "preview")
- }
+ override fun migrate(db: SupportSQLiteDatabase) = seedDefaultFilters(db)
+ }
- private fun seedFilter(
- db: SupportSQLiteDatabase,
- id: String,
- name: String,
- word: String,
- ) {
- db.execSQL(
- "INSERT OR IGNORE INTO rules " +
- "(id, name, position, enabled, subscriptionId, titleContains, " +
- "titleExcludes, minDurationMs, maxDurationMs, maxAgeMs, seasonNumber, " +
- "action, autoDownload, speed) " +
- "SELECT ?, ?, COALESCE(MAX(position), -1) + 1, 1, NULL, ?, NULL, " +
- "NULL, NULL, NULL, NULL, 'MARK_PLAYED', 0, NULL FROM rules",
- arrayOf(id, name, word),
- )
- }
+ /**
+ * The same filters for a fresh install, which never runs a migration.
+ *
+ * Found on a device: a reinstall creates the schema at the current version directly, so
+ * seeding only in MIGRATION_4_5 gave the filters to upgraders and silently to nobody
+ * else.
+ */
+ val SEED_DEFAULT_FILTERS = object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) = seedDefaultFilters(db)
}
fun get(context: Context): AppDatabase = instance ?: synchronized(this) {
@@ -143,6 +150,7 @@ abstract class AppDatabase : RoomDatabase() {
// 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)
+ .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
// loudly rather than silently wipe the library.