feat(data): schema v3 — an alarm or a timer that deletes itself after use

The `AlarmClock` contract says twice that an alarm or a timer created with
`SKIP_UI` should be removed once it has been dismissed. Without somewhere to
record that, a voice user's alarm list becomes a graveyard of one-shots nobody
asked to keep.

One column on each of `alarms` and `timers`, defaulting to 0, and a migration
that adds them. Existing rows keep the behaviour they have always had.
This commit is contained in:
2026-09-23 11:01:31 +02:00
parent cd26fefc69
commit 74a6295543
20 changed files with 598 additions and 12 deletions
@@ -35,6 +35,15 @@ class MigrationTest {
1700000000000, 1700000060000)
""".trimIndent()
private val insertTimer = """
INSERT INTO timers (
id, label, duration_millis, state, remaining_millis,
started_at_elapsed_realtime_millis, ends_at_elapsed_realtime_millis,
ends_at_wall_clock_millis, ringtone_uri, sort_order, created_at, updated_at
) VALUES (1, 'Pasta', 300000, 'IDLE', 300000, NULL, NULL, NULL, NULL, 0,
1700000000000, 1700000060000)
""".trimIndent()
private fun migratedDatabase(): ClockulaDatabase {
val db = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),
@@ -102,6 +111,33 @@ class MigrationTest {
assertThat(runBlocking { db.alarmStateDao().findByAlarmId(1L) }).isNull()
}
@Test
fun theSecondMigrationValidatesAgainstTheExportedVersionThreeSchema() {
helper.createDatabase(DB_NAME, 2).close()
val migrated = helper.runMigrationsAndValidate(DB_NAME, 3, true, Migrations.MIGRATION_2_3)
assertThat(migrated.version).isEqualTo(3)
migrated.close()
}
@Test
fun rowsWrittenAtVersionTwoSurviveAsPermanentOnes() {
helper.createDatabase(DB_NAME, 2).apply {
execSQL(insertAlarm)
execSQL(insertTimer)
close()
}
helper.runMigrationsAndValidate(DB_NAME, 3, true, Migrations.MIGRATION_2_3).close()
val db = migratedDatabase()
val alarm = runBlocking { db.alarmDao().findById(1L) }
val timer = runBlocking { db.timerDao().findById(1L) }
assertThat(alarm!!.deleteAfterUse).isFalse()
assertThat(timer!!.deleteAfterUse).isFalse()
}
private fun ringStateFor(alarmId: Long) = AlarmStateEntity(
alarmId = alarmId,
snoozedUntil = 1_700_000_100_000L,