README: what it is, reliability design, data model, backup format, build
Written for the repo's future reader (mostly me): why each reliability mechanism exists, how to decrypt a backup off-device with age -d, the FEST pipeline, and the keystore.properties signing convention. CLAUDE.md now points at it for architecture detail, per its own placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
293ca212ec
commit
8157922efd
2 changed files with 129 additions and 1 deletions
|
|
@ -91,4 +91,4 @@ All gradlew calls need `JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64` — system
|
|||
- The phone (Pixel 7 Pro) has a RELEASE-signed install: `adb install` of debug builds is
|
||||
rejected on signature; use release builds for the phone, emulator for debug.
|
||||
|
||||
<!-- Once a README exists, add: See @README.md for architecture detail. -->
|
||||
See @README.md for architecture detail.
|
||||
|
|
|
|||
128
README.md
Normal file
128
README.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# 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**.
|
||||
|
||||
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 is disabled (`allowBackup=false` + full
|
||||
`dataExtractionRules` opt-out) — health data leaves the device only through
|
||||
the app's own encrypted backup.
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue