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 <noreply@anthropic.com>
This commit is contained in:
commit
7256c49112
21 changed files with 845 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.gradle/
|
||||
build/
|
||||
local.properties
|
||||
.kotlin/
|
||||
*.iml
|
||||
.idea/
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
66
CLAUDE.md
Normal file
66
CLAUDE.md
Normal file
|
|
@ -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.
|
||||
|
||||
<!-- Personal prefs (24h, concise, explain-why, Linux-assumed) may already be in my
|
||||
global ~/.claude/CLAUDE.md. If so, trim the Working agreement here to avoid drift. -->
|
||||
|
||||
## 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`
|
||||
|
||||
<!-- Once a README exists, add: See @README.md for architecture detail. -->
|
||||
52
app/build.gradle.kts
Normal file
52
app/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
44
app/src/main/AndroidManifest.xml
Normal file
44
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Reminders are the point of this app; the permission set exists to keep them
|
||||
firing. See CLAUDE.md "Decisions" before touching any of these. -->
|
||||
|
||||
<!-- Exact alarms for dose times. USE_EXACT_ALARM (not SCHEDULE_EXACT_ALARM):
|
||||
medication reminders are alarm-clock-like core functionality, and this
|
||||
variant needs no runtime grant. Still gated on canScheduleExactAlarms(). -->
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- AlarmManager state is wiped on reboot; we re-arm everything from the DB. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!-- One-time prompt to exempt us from battery optimisation — the biggest cause
|
||||
of dropped reminders on aggressive OEMs (Samsung/Xiaomi). -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!-- Backups to self-hosted S3 (Garage) and FEST dataset refresh only. -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- No Google Auto Backup: this app runs its own encrypted backup. allowBackup
|
||||
covers pre-12; dataExtractionRules opts out of cloud backup AND device-to-
|
||||
device transfer on 12+. -->
|
||||
<application
|
||||
android:name=".MedDetSammeApp"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="false"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MedDetSamme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
36
app/src/main/java/no/naiv/meddetsamme/MainActivity.kt
Normal file
36
app/src/main/java/no/naiv/meddetsamme/MainActivity.kt
Normal file
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
10
app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt
Normal file
10
app/src/main/java/no/naiv/meddetsamme/MedDetSammeApp.kt
Normal file
|
|
@ -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()
|
||||
21
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
21
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- A capsule at 45°: two half-pill rounded shapes. Placeholder-quality, but ours. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:rotation="45"
|
||||
android:pivotX="54"
|
||||
android:pivotY="54">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M42,30 h24 a12,12 0 0 1 12,12 v0 h-48 v0 a12,12 0 0 1 12,-12 z"
|
||||
android:strokeWidth="0" />
|
||||
<path
|
||||
android:fillColor="#A5D6A7"
|
||||
android:pathData="M30,42 h48 v24 a12,12 0 0 1 -12,12 h-24 a12,12 0 0 1 -12,-12 z"
|
||||
android:strokeWidth="0" />
|
||||
</group>
|
||||
</vector>
|
||||
7
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
7
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- minSdk 26 = adaptive icons everywhere; no legacy PNG densities needed. -->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
4
app/src/main/res/values/colors.xml
Normal file
4
app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1B5E20</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values/strings.xml
Normal file
4
app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Med det samme</string>
|
||||
</resources>
|
||||
6
app/src/main/res/values/themes.xml
Normal file
6
app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Compose draws everything; this only styles the window before setContent.
|
||||
Platform theme, so no appcompat/material-components dependency. -->
|
||||
<style name="Theme.MedDetSamme" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Opt out of both Google cloud backup and device-to-device transfer (API 31+).
|
||||
Health data stays on-device; our own encrypted S3 backup is the only copy. -->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
7
build.gradle.kts
Normal file
7
build.gradle.kts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Root build file — plugin versions live in gradle/libs.versions.toml.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
}
|
||||
6
gradle.properties
Normal file
6
gradle.properties
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# JVM args for the Gradle daemon (not the app).
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
|
||||
android.useAndroidX=true
|
||||
41
gradle/libs.versions.toml
Normal file
41
gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Versions verified current-stable 2026-06-10 (Maven Central / Google Maven / release notes).
|
||||
# Kotlin pin matters: AGP 9.x bundles its own KGP (built-in Kotlin); the compose and
|
||||
# serialization compiler plugins must match that bundled compiler, NOT the newest Kotlin.
|
||||
# Kotlin 2.4.0 is out but KSP has no 2.4-compatible release yet — stay on 2.3.x.
|
||||
[versions]
|
||||
agp = "9.2.1"
|
||||
kotlin = "2.3.10"
|
||||
ksp = "2.3.9" # KSP versioning decoupled from Kotlin since 2.3.0 (no more <kotlin>-<ksp> pairs)
|
||||
composeBom = "2026.05.00"
|
||||
activityCompose = "1.13.0"
|
||||
coreKtx = "1.19.0"
|
||||
lifecycle = "2.10.0"
|
||||
room = "2.8.4" # Room 3 (androidx.room3) is still alpha — not for a medication app
|
||||
work = "2.11.2"
|
||||
okhttp = "5.4.0"
|
||||
serializationJson = "1.11.0"
|
||||
junit = "4.13.2"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = "work" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serializationJson" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
248
gradlew
vendored
Executable file
248
gradlew
vendored
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
93
gradlew.bat
vendored
Normal file
93
gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
147
med-det-samme-build-prompt.md
Normal file
147
med-det-samme-build-prompt.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# 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.
|
||||
18
settings.gradle.kts
Normal file
18
settings.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "med-det-samme"
|
||||
include(":app")
|
||||
Loading…
Add table
Add a link
Reference in a new issue