From 7eac15374061fcb580dfd9c934ca245a139e2461 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 17 Jun 2026 11:00:24 +0200 Subject: [PATCH 1/8] Add MIT license Release under MIT. Note that the bundled FEST extract stays under DMPs own open-data terms, not MIT, since it is third-party data shipped in the APK. Co-Authored-By: Claude Opus 4.8 --- LICENSE | 21 +++++++++++++++++++++ README.md | 8 ++++++++ 2 files changed, 29 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18cfc0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ole-Morten Duesund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4033bce..c7497ad 100644 --- a/README.md +++ b/README.md @@ -132,3 +132,11 @@ build is unsigned rather than failing. Caregiver alerts, multi-profile, Health Connect, streaks/gamification, symptom diary. This is one person's medication reminder, not a platform. + +## License + +[MIT](LICENSE) © Ole-Morten Duesund. + +The bundled FEST extract (`app/src/main/assets/fest/slim.json`) is derived from +DMP's openly published drug data and remains subject to DMP's own terms, not +the MIT license above. From bbf4d0af9c66d686a59888fa7e26788ff4b71ecd Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 17 Jun 2026 13:30:07 +0200 Subject: [PATCH 2/8] History: leading green check / red cross, and sane status labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each history row now starts with a green check (taken) or red cross (not taken), so the day scans at a glance. The split mirrors adherencePercent — only TAKEN counts as adhered — so the icons agree with the percentages on top. Also fixes the nonsense label: a past PENDING dose showed "Venter" as if it were still due. A history-local historyLabel maps PENDING -> "Ikke tatt"; the shared DoseStatus.label is untouched, so Today still shows "Venter". The fixed takenGreen/missedRed are deliberately not derived from the palette, or dynamic Material You colours could tint a "taken" tick non-green. Colour is paired with shape (check vs cross) and text, so meaning never rests on colour alone (WCAG 1.4.1). The trailing tonal DoseStatusBadge gave way to a muted text label, since the leading icon now carries the colour signal. Co-Authored-By: Claude Opus 4.8 --- .../no/naiv/meddetsamme/ui/HistoryScreen.kt | 28 +++++++++++++++++-- .../main/java/no/naiv/meddetsamme/ui/Theme.kt | 13 +++++++++ app/src/main/res/drawable/ic_check.xml | 9 ++++++ app/src/main/res/drawable/ic_close.xml | 9 ++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_check.xml create mode 100644 app/src/main/res/drawable/ic_close.xml diff --git a/app/src/main/java/no/naiv/meddetsamme/ui/HistoryScreen.kt b/app/src/main/java/no/naiv/meddetsamme/ui/HistoryScreen.kt index f2a9efa..ecd7c69 100644 --- a/app/src/main/java/no/naiv/meddetsamme/ui/HistoryScreen.kt +++ b/app/src/main/java/no/naiv/meddetsamme/ui/HistoryScreen.kt @@ -37,11 +37,21 @@ import java.time.format.DateTimeFormatter import java.util.Locale import no.naiv.meddetsamme.R import no.naiv.meddetsamme.data.DoseLog +import no.naiv.meddetsamme.data.DoseStatus import no.naiv.meddetsamme.data.MedDatabase import no.naiv.meddetsamme.domain.ScheduleEngine private const val HISTORY_DAYS = 30L +/** + * Status text in a *past* context. Differs from [label] only for PENDING: on + * the Today screen a due dose is legitimately "Venter", but a dose from days + * ago that was never acted on was simply not taken — "Venter" there is the + * nonsense this screen used to show. + */ +private val DoseStatus.historyLabel: String + get() = if (this == DoseStatus.PENDING) "Ikke tatt" else label + /** * Adherence history: the last 30 days of dose logs, newest first, with * 7- and 30-day adherence percentages on top. Read-only — corrections happen @@ -130,22 +140,36 @@ fun HistoryScreen(nav: Nav) { } items(dayLogs, key = { it.first.id }) { (log, medName) -> val time = timeFmt.format(Instant.ofEpochMilli(log.scheduledAtMillis).atZone(zone)) + val taken = log.status == DoseStatus.TAKEN Card( modifier = Modifier.fillMaxWidth().semantics(mergeDescendants = true) { - contentDescription = "$time, $medName, ${log.status.label}" + contentDescription = "$time, $medName, ${log.status.historyLabel}" }, ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { + // Green ✓ taken / red ✕ not-taken, first on the line for an + // at-a-glance scan down the day. Decorative here — the row's + // merged contentDescription already speaks the status. + Icon( + painterResource(if (taken) R.drawable.ic_check else R.drawable.ic_close), + contentDescription = null, + tint = if (taken) takenGreen() else missedRed(), + modifier = Modifier.padding(end = 12.dp), + ) Text(time, style = MaterialTheme.typography.titleSmall) Text( medName, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(start = 12.dp).weight(1f), ) - DoseStatusBadge(log.status) + Text( + log.status.historyLabel, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } diff --git a/app/src/main/java/no/naiv/meddetsamme/ui/Theme.kt b/app/src/main/java/no/naiv/meddetsamme/ui/Theme.kt index 1b0086f..2151745 100644 --- a/app/src/main/java/no/naiv/meddetsamme/ui/Theme.kt +++ b/app/src/main/java/no/naiv/meddetsamme/ui/Theme.kt @@ -37,6 +37,19 @@ fun MedDetSammeTheme(content: @Composable () -> Unit) { MaterialTheme(colorScheme = scheme, typography = AppTypography, content = content) } +/** + * Fixed adherence semantics for the history glance icons: green = taken, + * red = not taken. Deliberately *not* derived from the (possibly dynamic) + * brand palette — a wallpaper-blue "primary" must never make a "taken" tick + * read as anything but green. Paired with a distinct shape (✓ vs ✕) and text + * so meaning never rests on colour alone (WCAG 1.4.1). + */ +@Composable +fun takenGreen(): Color = if (isSystemInDarkTheme()) Color(0xFF7CC97F) else Color(0xFF2E7D32) + +@Composable +fun missedRed(): Color = if (isSystemInDarkTheme()) Color(0xFFFFB4AB) else Color(0xFFBA1A1A) + // Manrope for headings/titles (friendly, confident), Inter for body/labels // (built for UI legibility at small sizes). Both OFL, full æøå/µ coverage. private val Manrope = FontFamily( diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..8b4fd8d --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..7184acd --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + + From b4c929eca234f01c82281c7f7bb31c104996dda3 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 17 Jun 2026 13:39:55 +0200 Subject: [PATCH 3/8] Release 0.4.0: bump versionCode 4 / versionName 0.4.0 Co-Authored-By: Claude Opus 4.8 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1010f4..e45b245 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ android { targetSdk = 35 // Bump both for every release installed on the phone — versionName for // humans, versionCode so dumpsys/install history can tell builds apart. - versionCode = 3 - versionName = "0.3.0" + versionCode = 4 + versionName = "0.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } From d81a4fd9261e33ed01f0721a8504b6ba0d0a9967 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 24 Jun 2026 11:09:46 +0200 Subject: [PATCH 4/8] Seasonal (yearly) dosing for allergy/pollen meds Add a recurring month-day season window to DoseTime (seasonStartMmdd/ seasonEndMmdd): a med rests outside its window and re-arms automatically every year. The pure ScheduleEngine gains a season filter plus one-year overrides -- startEpochDay/endEpochDay act as early-start/extend brackets for seasonal rows while staying hard bounds for tapers. - Daily SupplyCheck nudges before a season opens (Start naa) and before it closes (Forleng), handled by the new SeasonActionReceiver. - Med list lists resting seasonal meds in a "Sesong (hviler)" section. - Dose-time editor gains a season block and a "scroll for more" hint so cyclic/season below the time picker are discoverable. - Room migration v3->v4 (additive), backup DTO extended, engine/text tests. Co-Authored-By: Claude Opus 4.8 --- .../4.json | 389 ++++++++++++++++++ .../no/naiv/meddetsamme/data/MigrationTest.kt | 29 ++ app/src/main/AndroidManifest.xml | 1 + .../meddetsamme/alarm/SeasonActionReceiver.kt | 66 +++ .../meddetsamme/alarm/SupplyCheckReceiver.kt | 74 +++- .../meddetsamme/backup/BackupSerializer.kt | 5 + .../java/no/naiv/meddetsamme/data/DoseTime.kt | 13 +- .../no/naiv/meddetsamme/data/MedDatabase.kt | 15 +- .../naiv/meddetsamme/domain/ScheduleEngine.kt | 110 ++++- .../naiv/meddetsamme/domain/ScheduleText.kt | 17 +- .../naiv/meddetsamme/notify/Notifications.kt | 46 +++ .../no/naiv/meddetsamme/ui/MedEditScreen.kt | 239 ++++++++--- .../no/naiv/meddetsamme/ui/MedListScreen.kt | 95 ++++- .../meddetsamme/domain/ScheduleEngineTest.kt | 96 +++++ .../meddetsamme/domain/ScheduleTextTest.kt | 12 + 15 files changed, 1101 insertions(+), 106 deletions(-) create mode 100644 app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json create mode 100644 app/src/main/java/no/naiv/meddetsamme/alarm/SeasonActionReceiver.kt diff --git a/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json b/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json new file mode 100644 index 0000000..45f17c4 --- /dev/null +++ b/app/schemas/no.naiv.meddetsamme.data.MedDatabase/4.json @@ -0,0 +1,389 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "56932b6862c83744cc926f80ab7875e0", + "entities": [ + { + "tableName": "inventory_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `strength` TEXT NOT NULL, `unit` TEXT NOT NULL, `form` TEXT NOT NULL, `atcCode` TEXT, `packageSize` REAL, `stockUnits` REAL NOT NULL, `lowStockLeadDays` INTEGER NOT NULL, `rxExpiryEpochDay` INTEGER, `refillsRemaining` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "strength", + "columnName": "strength", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unit", + "columnName": "unit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "form", + "columnName": "form", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atcCode", + "columnName": "atcCode", + "affinity": "TEXT" + }, + { + "fieldPath": "packageSize", + "columnName": "packageSize", + "affinity": "REAL" + }, + { + "fieldPath": "stockUnits", + "columnName": "stockUnits", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lowStockLeadDays", + "columnName": "lowStockLeadDays", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rxExpiryEpochDay", + "columnName": "rxExpiryEpochDay", + "affinity": "INTEGER" + }, + { + "fieldPath": "refillsRemaining", + "columnName": "refillsRemaining", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "medication", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `itemId` INTEGER NOT NULL, `withFood` INTEGER NOT NULL, `notes` TEXT NOT NULL, `active` INTEGER NOT NULL, FOREIGN KEY(`itemId`) REFERENCES `inventory_item`(`id`) ON UPDATE NO ACTION ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "withFood", + "columnName": "withFood", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_medication_itemId", + "unique": false, + "columnNames": [ + "itemId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_medication_itemId` ON `${TABLE_NAME}` (`itemId`)" + } + ], + "foreignKeys": [ + { + "table": "inventory_item", + "onDelete": "RESTRICT", + "onUpdate": "NO ACTION", + "columns": [ + "itemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "dose_time", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `medId` INTEGER NOT NULL, `minuteOfDay` INTEGER NOT NULL, `amount` REAL NOT NULL, `daysOfWeekMask` INTEGER NOT NULL, `intervalDays` INTEGER NOT NULL, `anchorEpochDay` INTEGER, `cycleActiveDays` INTEGER NOT NULL DEFAULT 0, `cycleLengthDays` INTEGER NOT NULL DEFAULT 0, `startEpochDay` INTEGER, `endEpochDay` INTEGER, `seasonStartMmdd` INTEGER NOT NULL DEFAULT 0, `seasonEndMmdd` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`medId`) REFERENCES `medication`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "medId", + "columnName": "medId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minuteOfDay", + "columnName": "minuteOfDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "daysOfWeekMask", + "columnName": "daysOfWeekMask", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "intervalDays", + "columnName": "intervalDays", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anchorEpochDay", + "columnName": "anchorEpochDay", + "affinity": "INTEGER" + }, + { + "fieldPath": "cycleActiveDays", + "columnName": "cycleActiveDays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "cycleLengthDays", + "columnName": "cycleLengthDays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startEpochDay", + "columnName": "startEpochDay", + "affinity": "INTEGER" + }, + { + "fieldPath": "endEpochDay", + "columnName": "endEpochDay", + "affinity": "INTEGER" + }, + { + "fieldPath": "seasonStartMmdd", + "columnName": "seasonStartMmdd", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "seasonEndMmdd", + "columnName": "seasonEndMmdd", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_dose_time_medId", + "unique": false, + "columnNames": [ + "medId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_time_medId` ON `${TABLE_NAME}` (`medId`)" + } + ], + "foreignKeys": [ + { + "table": "medication", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "medId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "dose_log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `medId` INTEGER NOT NULL, `doseTimeId` INTEGER, `scheduledAtMillis` INTEGER NOT NULL, `amount` REAL NOT NULL, `status` TEXT NOT NULL, `actionedAtMillis` INTEGER, `nagCount` INTEGER NOT NULL, FOREIGN KEY(`medId`) REFERENCES `medication`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`doseTimeId`) REFERENCES `dose_time`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "medId", + "columnName": "medId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "doseTimeId", + "columnName": "doseTimeId", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAtMillis", + "columnName": "scheduledAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionedAtMillis", + "columnName": "actionedAtMillis", + "affinity": "INTEGER" + }, + { + "fieldPath": "nagCount", + "columnName": "nagCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_dose_log_medId", + "unique": false, + "columnNames": [ + "medId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_medId` ON `${TABLE_NAME}` (`medId`)" + }, + { + "name": "index_dose_log_doseTimeId", + "unique": false, + "columnNames": [ + "doseTimeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_doseTimeId` ON `${TABLE_NAME}` (`doseTimeId`)" + }, + { + "name": "index_dose_log_doseTimeId_scheduledAtMillis", + "unique": false, + "columnNames": [ + "doseTimeId", + "scheduledAtMillis" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_doseTimeId_scheduledAtMillis` ON `${TABLE_NAME}` (`doseTimeId`, `scheduledAtMillis`)" + }, + { + "name": "index_dose_log_status", + "unique": false, + "columnNames": [ + "status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_dose_log_status` ON `${TABLE_NAME}` (`status`)" + } + ], + "foreignKeys": [ + { + "table": "medication", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "medId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "dose_time", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "doseTimeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '56932b6862c83744cc926f80ab7875e0')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt b/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt index e57272e..0a46990 100644 --- a/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt +++ b/app/src/androidTest/java/no/naiv/meddetsamme/data/MigrationTest.kt @@ -108,6 +108,35 @@ class MigrationTest { } } + @Test + fun migrate3To4_addsSeasonColumnsDefaultingToNoSeason() { + helper.createDatabase(TEST_DB, 3).use { db -> + db.execSQL( + "INSERT INTO inventory_item (id, name, strength, unit, form, stockUnits, " + + "lowStockLeadDays) VALUES (1, 'Cetirizin', '10 mg', 'tablett', 'TABLET', 30.0, 7)", + ) + db.execSQL("INSERT INTO medication (id, itemId, withFood, notes, active) VALUES (1, 1, 0, '', 1)") + db.execSQL( + "INSERT INTO dose_time (id, medId, minuteOfDay, amount, daysOfWeekMask, " + + "intervalDays, anchorEpochDay, cycleActiveDays, cycleLengthDays) " + + "VALUES (3, 1, 480, 1.0, 127, 0, NULL, 0, 0)", + ) + } + + // Validates the migrated schema against 4.json AND checks the new columns + // default to "no season" while the existing row survives. + helper.runMigrationsAndValidate(TEST_DB, 4, true, MedDatabase.MIGRATION_3_4).use { db -> + db.query( + "SELECT amount, seasonStartMmdd, seasonEndMmdd FROM dose_time WHERE id = 3", + ).use { c -> + assertTrue(c.moveToFirst()) + assertEquals(1.0, c.getDouble(0), 0.0) + assertEquals(0, c.getInt(1)) // seasonStartMmdd + assertEquals(0, c.getInt(2)) // seasonEndMmdd + } + } + } + private companion object { const val TEST_DB = "migration-test.db" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0895d71..dba3ffc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,7 @@ +