147 lines
9.1 KiB
Markdown
147 lines
9.1 KiB
Markdown
|
|
# Build prompt — "Med det samme" (Android medication reminder)
|
||
|
|
|
||
|
|
You are building a native Android app from scratch. Read this whole brief first,
|
||
|
|
restate your build plan as milestones, and wait for my go-ahead before writing code.
|
||
|
|
|
||
|
|
## What and why
|
||
|
|
A local-first medication reminder — my personal replacement for MyTherapy, which I
|
||
|
|
left over ads and tracking. Single user, single device, no account, no analytics,
|
||
|
|
no cloud service of yours. Reminders are the entire point, so reliability beats
|
||
|
|
polish everywhere there's a trade-off. The name "Med det samme" ("right away") is
|
||
|
|
the design brief in three words: it nags you to take the dose *now*.
|
||
|
|
|
||
|
|
- applicationId / package: `no.naiv.meddetsamme`
|
||
|
|
- Display name: `Med det samme` (in `strings.xml`, referenced from the manifest)
|
||
|
|
- minSdk 26, targetSdk 35, JVM 17, Kotlin + Jetpack Compose
|
||
|
|
|
||
|
|
## How I want you to work
|
||
|
|
- Linux dev host. I'm a senior dev — be concise, skip hand-holding, explain *why*
|
||
|
|
for non-obvious choices in a line or two (XAI, not essays). 24h clocks everywhere.
|
||
|
|
- Build in the milestones below, one at a time. After each, run the build/tests and
|
||
|
|
show me it actually compiles — never claim it builds without running it.
|
||
|
|
- Verify current stable versions of every library yourself (web search if needed)
|
||
|
|
before pinning them in a Gradle version catalog. Don't trust versions from memory.
|
||
|
|
- This is a medication app: wrong schedule/dose math is a real harm. The scheduling
|
||
|
|
logic must be pure and unit-tested before anything depends on it.
|
||
|
|
- Ask before anything irreversible (schema you'll have to migrate, etc.). Prefer the
|
||
|
|
smallest change that works; no speculative abstraction.
|
||
|
|
|
||
|
|
## Stack
|
||
|
|
Kotlin, Compose (Material3), Room (+KSP), WorkManager, OkHttp, AndroidX
|
||
|
|
security-crypto, kotlinx-serialization. Gradle Kotlin DSL with a version catalog.
|
||
|
|
No AWS SDK. No third-party DI framework — manual wiring is fine at this size.
|
||
|
|
|
||
|
|
## Non-negotiable decisions (don't relitigate; flag and wait if you disagree)
|
||
|
|
1. **Native, not PWA.** PWA notification reliability is unacceptable for medication.
|
||
|
|
2. **Exact alarms** via `USE_EXACT_ALARM`, always gated on `canScheduleExactAlarms()`
|
||
|
|
with a graceful inexact fallback; use `setExactAndAllowWhileIdle` so Doze doesn't
|
||
|
|
swallow doses.
|
||
|
|
3. **Re-arm on boot and app update** (`BOOT_COMPLETED` + `MY_PACKAGE_REPLACED`).
|
||
|
|
AlarmManager state is wiped then; rebuild every alarm from the DB.
|
||
|
|
4. **Battery-optimisation exemption**: declare the permission and surface a one-time
|
||
|
|
runtime prompt. This is the biggest cause of dropped reminders on Samsung/Xiaomi.
|
||
|
|
5. **No Google Auto Backup** — `allowBackup="false"`, exclude from cloud-backup and
|
||
|
|
device-transfer. We run our own backup.
|
||
|
|
6. **Own backup**: versioned JSON → encrypted → S3-compatible PUT to a self-hosted
|
||
|
|
**Garage** bucket, path-style, with a hand-rolled SigV4 signer. One serializer
|
||
|
|
serves export, import, and auto-backup.
|
||
|
|
7. **Crypto**: target is **age** (passphrase/scrypt mode) so backups are decryptable
|
||
|
|
from the CLI with `age -d` and never lock me into this app. Ship a working
|
||
|
|
dependency-free baseline first (PBKDF2-HMAC-SHA256 → AES-256-GCM, self-describing
|
||
|
|
header) behind an interface, then implement the age path (Jagged or kage) and
|
||
|
|
verify it against the age Community Cryptography Test Vectors before trusting it.
|
||
|
|
8. **FEST, not Felleskatalogen**, for drug data — and **not** as a live API. See below.
|
||
|
|
9. **Refill is derived** from inventory + consumption rate, not a manual reminder.
|
||
|
|
**Prescription renewal is tracked separately from stock** (you can have pills but a
|
||
|
|
dead e-resept).
|
||
|
|
|
||
|
|
## Features
|
||
|
|
Core: multiple medications (tablet/capsule/liquid/injection/drops/spray/other),
|
||
|
|
flexible schedules, dose logging (taken/skipped/snoozed) → adherence history,
|
||
|
|
inventory tracking with derived refill warnings, encrypted export/import + automatic
|
||
|
|
backup.
|
||
|
|
|
||
|
|
Plus these four (deliberately chosen from a MyTherapy feature review):
|
||
|
|
- **Escalating reminders**: on a due dose, create a PENDING log and re-notify every
|
||
|
|
10 min (cap ~6 times / ~1 h) until I tap Taken or Snooze. Taken cancels escalation
|
||
|
|
and decrements inventory; Snooze pushes the next nag out 15 min and leaves it PENDING.
|
||
|
|
- **Per-day / weekend-different times**: model a dose-time as time-of-day + a
|
||
|
|
day-of-week bitmask (+ an every-N-days option). Multiple rows per med give different
|
||
|
|
times on different days. Cyclic/taper should extend from the same model.
|
||
|
|
- **Doctor summary**: a human-readable one-page PDF (current meds, schedule, supply,
|
||
|
|
Rx expiry, recent adherence %) via the platform `PdfDocument` — no PDF dependency.
|
||
|
|
Distinct from the machine-readable JSON backup.
|
||
|
|
- **Rx renewal fields**: prescription expiry date and refills-remaining
|
||
|
|
(reiterutleveringer), independent of physical stock.
|
||
|
|
|
||
|
|
Out of scope — do **not** add without asking: caregiver/"Team" alerts, multi-profile,
|
||
|
|
Health Connect, streaks/gamification, injection-site tracking, symptom/mood diary.
|
||
|
|
(Model "trackable" generically if it's cheap, but build no UI for the diary stuff.)
|
||
|
|
|
||
|
|
## Data model (anchor, not gospel — improve if warranted, but flag changes)
|
||
|
|
- `Medication`: name, strength, unit, form, withFood, notes, inventoryUnits,
|
||
|
|
packageSize, lowStockLeadDays, rxExpiryEpochDay?, refillsRemaining?, atcCode?, active.
|
||
|
|
- `DoseTime`: medId(FK), minuteOfDay, amount, daysOfWeekMask (bit 0 = Sunday),
|
||
|
|
intervalDays, anchorEpochDay.
|
||
|
|
- `DoseLog`: medId, doseTimeId, scheduledAtMillis, amount, status
|
||
|
|
(PENDING/TAKEN/SKIPPED/SNOOZED), actionedAtMillis?.
|
||
|
|
|
||
|
|
Put all schedule math in a pure object (next-occurrence from mask+interval, average
|
||
|
|
daily consumption, days-of-supply, needs-refill, needs-renewal). The alarm layer
|
||
|
|
holds at most one pending alarm per dose-time and recomputes from the DB — so reboot,
|
||
|
|
update, and "taken" all just re-ask the engine. No drift.
|
||
|
|
|
||
|
|
## Backup details
|
||
|
|
- Slim S3 client: path-style `PUT https://endpoint/bucket/key`, SigV4 signed
|
||
|
|
(host + x-amz-content-sha256 + x-amz-date), `x-amz-content-sha256` = hex SHA-256 of
|
||
|
|
the body. Endpoint points at my tailnet host behind Caddy (TLS terminated there).
|
||
|
|
- WorkManager daily periodic job; inexact is fine (a late backup is harmless, unlike a
|
||
|
|
dose). Each backup is a distinct timestamped object; retention is handled by a
|
||
|
|
lifecycle Expiration rule on the bucket, not by the app.
|
||
|
|
- Credentials (endpoint, region [Garage default often "garage"], bucket, access/secret)
|
||
|
|
and the optional auto-backup passphrase in Keystore-backed
|
||
|
|
EncryptedSharedPreferences. Be honest in comments: auto-backup must store the
|
||
|
|
passphrase to run unattended, so it only protects against bucket compromise; manual
|
||
|
|
export should prompt for a typed passphrase and never store it.
|
||
|
|
|
||
|
|
## FEST drug lookup
|
||
|
|
FEST (the open national dataset from DMP, ex-Legemiddelverket) is the source.
|
||
|
|
Felleskatalogen has no open developer API and is licensed editorial content — don't
|
||
|
|
use it. FEST's open Rekvirent extract (human-use meds) is a SOAP/WCF XML dump (the
|
||
|
|
M30 message), **not** a per-keystroke REST API — so do **not** call it from the phone.
|
||
|
|
|
||
|
|
App side: read a slim, pre-flattened JSON dataset (name, strength, unit, form, ATC,
|
||
|
|
package size) and do offline autocomplete against it. The job that pulls the M30 on
|
||
|
|
my server, flattens it, and publishes that JSON (via Caddy or into the Garage bucket)
|
||
|
|
lives outside this repo — just document the expected JSON shape and where the app
|
||
|
|
loads it from. Refresh is monthly-ish; FEST changes slowly.
|
||
|
|
|
||
|
|
## Milestones (verify each before moving on)
|
||
|
|
1. Project skeleton: Gradle + version catalog (current stable, verified), manifest
|
||
|
|
with the permission set, Application class, empty Compose activity. `assembleDebug`
|
||
|
|
green.
|
||
|
|
2. Data layer: Room entities, DAO, DB, converters. A couple of DAO instrumented sanity
|
||
|
|
checks if cheap.
|
||
|
|
3. Schedule engine (pure) + **unit tests**: weekly mask, every-N-days, supply, renewal.
|
||
|
|
4. Reminder subsystem: notifications (channels, Taken/Snooze actions), AlarmScheduler
|
||
|
|
(next + escalation + daily supply check), DoseAlarmReceiver, DoseActionReceiver,
|
||
|
|
BootReceiver. Manually verify on a device: dose fires, escalates, Taken cancels +
|
||
|
|
decrements, survives reboot.
|
||
|
|
5. Backup: versioned JSON serializer (export/import), SigV4 + S3 client, crypto
|
||
|
|
interface + JCE baseline, WorkManager job, encrypted settings store.
|
||
|
|
6. age crypto behind the interface; verify against the age test vectors.
|
||
|
|
7. Doctor-summary PDF + a share action (FileProvider + ACTION_SEND).
|
||
|
|
8. FEST offline lookup + autocomplete field.
|
||
|
|
9. UI buildout: today's doses with Taken/Skip/Snooze, add/edit med, schedule editor,
|
||
|
|
settings (S3 + passphrase), share-summary. TalkBack-correct semantics as you go —
|
||
|
|
cheap now, painful to retrofit.
|
||
|
|
|
||
|
|
## Acceptance
|
||
|
|
`./gradlew assembleDebug` and `./gradlew test` both pass; the schedule engine has unit
|
||
|
|
tests; reminders verified manually through a reboot. When the repo's stable, distill
|
||
|
|
the non-negotiable decisions + working agreement above into a `CLAUDE.md` at the repo
|
||
|
|
root so they persist across sessions.
|
||
|
|
|
||
|
|
Practical note: easiest start is for me to create an empty Compose project in Android
|
||
|
|
Studio (so the Gradle wrapper and toolchain are valid), then you build everything out
|
||
|
|
inside it from milestone 1. Confirm your plan and I'll do that.
|