# Med det samme A local-first Android medication reminder. *«Med det samme»* — "right away" — is the design brief in three words: it nags you to take the dose **now**. The name tips its hat to Arve Opsahl's 1962 classic [*«Medisin, medisin med det samme!»*](https://sonichits.com/video/Arve_Opsahl/Medisin%2C_Medisin_Med_Det_Samme!) — a man demanding his medicine *right away*, which is exactly what this app does on his behalf. Built as a personal replacement for MyTherapy: no ads, no account, no analytics, no cloud service. Single user, single device. Reminders are the entire point, so reliability beats polish wherever the two trade off. ## Features - **Escalating reminders** — a due dose posts a notification and re-nags every 10 minutes (capped at ~6) until you tap *Tatt* or *Utsett*. Snooze defers 15 minutes and keeps nagging. The nag counter lives in the database, so a reboot mid-escalation resumes where it left off. - **Flexible schedules** — time-of-day + weekday mask (different times on different days) or every-N-days from an anchor date. All schedule math is a pure, unit-tested engine. - **Inventory** — physical stock is its own entity with one-tap package restock. Taking a dose decrements stock; refill warnings are *derived* from stock ÷ scheduled consumption, never manually scheduled. Prescription renewal (expiry, reiterutleveringer) is tracked separately from stock — you can have pills and a dead e-resept, or vice versa. - **Offline drug autocomplete** — a slim extract of [FEST](https://www.dmp.no/om-oss/distribusjon-av-legemiddeldata/fest) (the open national dataset from DMP) is bundled in the APK and searched offline. Release builds auto-refresh the bundle when it's >30 days old. - **Encrypted backups you can leave with** — versioned JSON, encrypted as a standard [age](https://age-encryption.org) v1 file (scrypt/passphrase mode), uploaded nightly to a self-hosted S3-compatible bucket ([Garage](https://garagehq.deuxfleurs.fr/)) with a hand-rolled SigV4 signer. Any backup decrypts on any machine with plain `age -d` — no app lock-in. - **Doctor summary** — a one-page PDF (current meds, schedules, supply, rx status, 30-day adherence) generated with the platform `PdfDocument` and shared via the system sheet. Human-readable, distinct from the machine-readable backup. ## Reliability design Android actively fights background work; a medication app has to fight back: - **Exact alarms** (`USE_EXACT_ALARM`, plus `SCHEDULE_EXACT_ALARM` on API 31–32) with `setExactAndAllowWhileIdle`, always gated on `canScheduleExactAlarms()` with an inexact fallback. - **Re-arm on boot and app update** (`BOOT_COMPLETED`, `MY_PACKAGE_REPLACED`) — AlarmManager state is wiped on both; every alarm is rebuilt from the DB. - **No alarm drift by construction** — the alarm layer holds at most one pending alarm per dose-time and always *recomputes* from the database via the pure engine. Reboot, update and "Taken" all just re-ask. PendingIntent identity (stable requestCode + `FLAG_UPDATE_CURRENT`) makes stacking impossible at the OS level. - **One-time battery-optimisation exemption prompt** — the biggest cause of dropped reminders on aggressive OEMs. - **One implementation of Taken/Snooze/Skip** (`DoseActions`), shared by the notification actions and the UI, so the two can never disagree about inventory or escalation state. ## Data model ``` InventoryItem — physical stock: name, strength, unit, form, ATC, package size, stock, low-stock lead, rx expiry/refills Medication — a dosing regimen referencing an item (RESTRICT FK: deleting stock can never erase adherence history) DoseTime — minute-of-day + weekday mask (bit 0 = Sunday) or every-N-days from an anchor epoch day DoseLog — one scheduled occurrence and its outcome (PENDING/TAKEN/SKIPPED/SNOOZED), amount snapshotted ScheduleEngine — pure: next occurrence, daily consumption, days-of-supply, needs-refill, needs-renewal, adherence ``` Room with exported schemas committed under `app/schemas/`; migrations are proven by instrumented `MigrationTestHelper` tests before they touch real data. Google Auto Backup carries exactly one file: the app's own age-encrypted export (written only while a backup passphrase is set) — include-rules keep the database and settings out, and restoring on a new device requires typing the passphrase, since Keystore keys never migrate. ## Backup format `{"version": 2, "inventoryItems": [...], "medications": [...], "doseTimes": [...], "doseLogs": [...]}` — pretty-printed JSON, then encrypted. Decrypt anywhere: ```sh age -d backup-20260610T020000Z.json.age | jq . ``` Version 1 files (pre inventory split) still import. The JCE baseline format (`MDS1` header, PBKDF2-HMAC-SHA256 → AES-256-GCM) remains readable; new backups are always age. The age implementation ([kage](https://github.com/android-password-store/kage)) is verified in unit tests against the [C2SP CCTV](https://github.com/C2SP/CCTV) scrypt vectors. ## FEST dataset `tools/fest_flatten.py` (stdlib Python) downloads DMP's M30 Rekvirent extract, streams the ~115 MB XML, and flattens ~9 000 active human-use preparations into `app/src/main/assets/fest/slim.json`. `assembleRelease` runs it automatically: fresh file → fast skip; download failure → keep the old file (an offline release build never breaks). See `docs/fest-dataset.md` for the JSON contract. Felleskatalogen is deliberately not used: licensed editorial content, no open API. ## Building Requirements: JDK 17 and an Android SDK with platform 37. ```sh ./gradlew assembleDebug # debug build ./gradlew test # unit tests (engine, crypto, SigV4, FEST search) ./gradlew connectedDebugAndroidTest # migration tests (needs an emulator/device) ./gradlew assembleRelease # signed if keystore.properties exists, else unsigned ``` Release signing reads a gitignored `keystore.properties` at the repo root (`storeFile`/`storePassword`/`keyAlias`/`keyPassword`); without it the release build is unsigned rather than failing. - minSdk 26, targetSdk 35, compileSdk 37 - Kotlin + Jetpack Compose (Material3), Room (+KSP), WorkManager, OkHttp, kotlinx-serialization, kage — Gradle Kotlin DSL with a version catalog, no DI framework, no AWS SDK. ## Out of scope 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.