Medisin medisin med det samme. En mindre masete løsning enn andre.
  • Kotlin 97.8%
  • Python 2.2%
Find a file
Ole-Morten Duesund 5b9a3d6877 Release 0.5.0: bump versionCode 5 / versionName 0.5.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:09:46 +02:00
app Release 0.5.0: bump versionCode 5 / versionName 0.5.0 2026-06-24 11:09:46 +02:00
docs Record backup verification: Google new-device demo + test coverage 2026-06-11 21:49:54 +02:00
gradle New launcher icon, version 0.2.0, lint cleanup 2026-06-11 17:55:06 +02:00
tools FEST: in-repo flattener bundles slim.json; release builds auto-refresh when stale 2026-06-10 15:19:49 +02:00
.gitignore Release signing from gitignored keystore.properties 2026-06-10 14:27:40 +02:00
build.gradle.kts Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
CLAUDE.md CLAUDE.md: build/test gotchas from the cyclic-taper + backup sessions 2026-06-11 22:19:50 +02:00
gradle.properties Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
gradlew Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
gradlew.bat Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
LICENSE Add MIT license 2026-06-17 11:00:24 +02:00
med-det-samme-build-prompt.md Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
README.md Add MIT license 2026-06-17 11:00:24 +02:00
settings.gradle.kts Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set 2026-06-10 13:31:39 +02:00
THIRD_PARTY_LICENSES.md Typography: Manrope headings, Inter body — plus norsk dag/dager pluralization 2026-06-11 17:09:07 +02:00

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!» — 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 (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 v1 file (scrypt/passphrase mode), uploaded nightly to a self-hosted S3-compatible bucket (Garage) 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 3132) 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:

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) is verified in unit tests against the 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.

./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 © 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.