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:
Ole-Morten Duesund 2026-06-10 13:31:39 +02:00
commit 7256c49112
21 changed files with 845 additions and 0 deletions

52
app/build.gradle.kts Normal file
View 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)
}

View 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>

View 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),
)
}
}

View 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()

View 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>

View 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>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B5E20</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Med det samme</string>
</resources>

View 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>

View 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>