From 7256c491124cbd45fee4e6d54628c8e853a242df Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 10 Jun 2026 13:31:39 +0200 Subject: [PATCH] Project skeleton: Gradle 9.4.1 wrapper, AGP 9.2.1, Compose, manifest with reminder permission set Milestone 1 of the build brief. Version catalog pins verified against Maven Central / Google Maven on 2026-06-10; Kotlin held at 2.3.10 to match AGP 9.2's bundled KGP (built-in Kotlin) because KSP has no Kotlin 2.4 release yet. compileSdk 37 (forced by core-ktx 1.19), targetSdk 35 per brief. allowBackup=false + full dataExtractionRules opt-out; own encrypted backup comes in milestone 5. Co-Authored-By: Claude Fable 5 --- .gitignore | 9 + CLAUDE.md | 66 +++++ app/build.gradle.kts | 52 ++++ app/src/main/AndroidManifest.xml | 44 ++++ .../java/no/naiv/meddetsamme/MainActivity.kt | 36 +++ .../no/naiv/meddetsamme/MedDetSammeApp.kt | 10 + .../res/drawable/ic_launcher_foreground.xml | 21 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 + app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/themes.xml | 6 + .../main/res/xml/data_extraction_rules.xml | 19 ++ build.gradle.kts | 7 + gradle.properties | 6 + gradle/libs.versions.toml | 41 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 93 +++++++ med-det-samme-build-prompt.md | 147 +++++++++++ settings.gradle.kts | 18 ++ 21 files changed, 845 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 app/build.gradle.kts create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/no/naiv/meddetsamme/MainActivity.kt create mode 100644 app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 med-det-samme-build-prompt.md create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3decb9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +build/ +local.properties +.kotlin/ +*.iml +.idea/ +captures/ +.externalNativeBuild/ +.cxx/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d3b29e9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# «Med det samme» — Claude Code project memory + +Local-first Android medication reminder. My personal MyTherapy replacement: no ads, +no account, no analytics, no cloud service of yours. Single user, single device. +Reminders are the entire point — reliability beats polish wherever they trade off. +The name says the brief: nag me to take the dose *now*. + +- Package / applicationId: `no.naiv.meddetsamme` +- Display name: `Med det samme` (in `strings.xml`, referenced from the manifest) +- minSdk 26, targetSdk 35, JVM 17, Kotlin + Compose (Material3) +- Stack: Room (+KSP), WorkManager, OkHttp, security-crypto, kotlinx-serialization. + Gradle Kotlin DSL + version catalog. No AWS SDK, no DI framework. + + + +## Working agreement +- Senior dev, Linux host. Be concise; explain *why* for non-obvious choices in a line + or two (XAI, not essays). 24h clocks everywhere. +- Never claim it builds without running the build. Verify; don't assert. +- Verify current stable library versions yourself before bumping — don't trust memory. +- Smallest change that works. No speculative abstraction. +- Ask before anything irreversible (schema migrations, data deletion). +- This is a medication app: wrong schedule/dose math is a real harm. Keep schedule + logic pure and unit-tested before anything depends on it. + +## Decisions — don't relitigate (flag and wait if you think one's wrong) +1. Native, not PWA — PWA notification reliability is unacceptable for meds. +2. Exact alarms (`USE_EXACT_ALARM`), always gated on `canScheduleExactAlarms()` with an + inexact fallback; `setExactAndAllowWhileIdle` so Doze doesn't eat doses. +3. Re-arm every alarm on `BOOT_COMPLETED` + `MY_PACKAGE_REPLACED` (state is wiped then). +4. Surface a one-time battery-optimisation exemption prompt — biggest cause of dropped + reminders on aggressive OEMs. +5. No Google Auto Backup (`allowBackup="false"`, excluded from cloud-backup + transfer). +6. Own backup: versioned JSON → encrypted → S3-compatible PUT to self-hosted **Garage**, + path-style, hand-rolled SigV4. One serializer for export, import, and auto-backup. +7. Crypto target is **age** (passphrase/scrypt) so backups are `age -d`-decryptable and + never lock me into this app. JCE AES-GCM is the baseline behind an interface; verify + age against the age test vectors before trusting it. +8. **FEST, not Felleskatalogen**, and **not** as a live API: its open Rekvirent extract + is a SOAP/WCF M30 XML dump. The phone reads a slim pre-flattened JSON synced from my + server (that job lives outside this repo) and does autocomplete offline. +9. Refill is **derived** from inventory + consumption. Prescription renewal + (`rxExpiryEpochDay`, `refillsRemaining`) is tracked **separately** from stock. + +## Load-bearing — change with care +- The reminder subsystem is the reliability core. After any change, re-verify: alarms + re-arm on boot/update, escalation cancels on Taken, the next occurrence is always + armed, Taken decrements inventory. +- Escalation contract: due dose → PENDING log + notify, re-nag every 10 min (cap ~6 / + ~1 h) until Taken or Snooze; Snooze pushes the next nag 15 min and stays PENDING. +- The S3 SigV4 signer: if a PUT fails it's almost always clock skew or a non-path-style + endpoint, not the maths. Don't "fix" the signer first. +- The schedule engine is pure — add tests here before touching the math. + +## Out of scope — don't add without asking +Caregiver/"Team" alerts, multi-profile, Health Connect, streaks/gamification, +injection-site tracking, symptom/mood diary. (Modelling "trackable" generically is fine +if cheap; build no diary UI.) + +## Commands +- Build debug: `./gradlew assembleDebug` +- Unit tests: `./gradlew test` +- Lint: `./gradlew lint` + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..bd4f8ea --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.android.application) + // AGP 9 has built-in Kotlin (org.jetbrains.kotlin.android is gone); only the + // compiler sub-plugins are applied separately, pinned to AGP's bundled KGP version. + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "no.naiv.meddetsamme" + // compileSdk 37: required by current AndroidX (core 1.19 / compose BOM 2026.05). + // targetSdk stays 35 (brief) — compile-against vs runtime-behavior are separate knobs. + compileSdk = 37 + + defaultConfig { + applicationId = "no.naiv.meddetsamme" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "0.1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + // Built-in Kotlin derives jvmTarget from targetCompatibility — one knob, not two. + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.material3) + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) + + testImplementation(libs.junit) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3bd1670 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/no/naiv/meddetsamme/MainActivity.kt b/app/src/main/java/no/naiv/meddetsamme/MainActivity.kt new file mode 100644 index 0000000..fa0645e --- /dev/null +++ b/app/src/main/java/no/naiv/meddetsamme/MainActivity.kt @@ -0,0 +1,36 @@ +package no.naiv.meddetsamme + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + AppRoot() + } + } + } +} + +@Composable +private fun AppRoot() { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Text( + text = "Med det samme", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(innerPadding), + ) + } +} diff --git a/app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt b/app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt new file mode 100644 index 0000000..a2b0b4c --- /dev/null +++ b/app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt @@ -0,0 +1,10 @@ +package no.naiv.meddetsamme + +import android.app.Application + +/** + * Application entry point. Later milestones hang app-wide singletons off this + * (DB, notification channels, alarm re-arm) — no DI framework at this size, + * manual wiring via a small service locator. + */ +class MedDetSammeApp : Application() diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..abb1f8c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5d589d0 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..61ff694 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #1B5E20 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..23dcdb0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Med det samme + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..3293cf2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + +