diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2baada3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,116 @@
+# Buschauffeurswekker
+
+Android-app die de functie van de normale wekker-app vervult, maar de wekkers
+automatisch zet op basis van de Nextcloud CalDAV-agenda **EBS-Diensten**.
+
+## Werking
+
+- Elke dag om **12:00** leest de app de agenda van de **volgende dag**.
+- De dienstbegintijd wordt **naar beneden afgerond op een heel kwartier**:
+ - 05:00–05:14 → 05:00
+ - 05:15–05:29 → 05:15
+ - 05:30–05:44 → 05:30
+ - 05:45–05:59 → 05:45
+- Er worden drie wekkers gezet, alle drie vóór de afgeronde aanvangstijd:
+ - **Opstaan** — 2 uur vóór aanvang
+ - **Wally** — 1,5 uur vóór aanvang
+ - **Vertrek** — 0,5 uur vóór aanvang
+- Staat er **geen dienst** in de agenda voor morgen, dan worden **geen wekkers** gezet.
+- De wekker gaat volwaardig af (geluid + trillen, volledig scherm) **ook bij een
+ vergrendeld scherm**, met snooze (10 min) en "wekker uit".
+- Het toestel mag daarvoor **mobiele data** verbruiken en taken op de
+ **achtergrond** uitvoeren.
+
+## Installatie
+
+1. Bouw de APK: `./gradlew :app:assembleDebug`
+ → `app/build/outputs/apk/debug/app-debug.apk`
+2. Zet de APK op de telefoon en installeer (sideload).
+3. Open de app en vul bij **Instellingen** in:
+ - Agenda-URL: `https://nextcloud.eddydevink.nl/remote.php/dav/calendars/eddy/ebs_diensten-1/`
+ - Gebruikersnaam: `eddy`
+ - App-wachtwoord: een Nextcloud-app-wachtwoord
+ (Nextcloud → Beveiliging → App-wachtwoorden; zie de Obsidian-vault voor
+ de bestaande app-wachtwoorden — niet in de app hardcoden)
+4. Druk op **Opslaan** en daarna op **Nu controleren** om te testen.
+5. Verleen de gevraagde machtigingen via de knoppen onder **Machtigingen**:
+ meldingen, exacte alarmen, batterijoptimalisatie uitzonderen en (Android 14+)
+ volledig scherm.
+
+## Machtigingen
+
+| Machtiging | Doel |
+|---|---|
+| Internet / netwerk | Agenda ophalen over wifi én mobiele data |
+| Meldingen (POST_NOTIFICATIONS) | Afgaande wekker tonen |
+| Exacte alarmen (USE_EXACT_ALARM / SCHEDULE_EXACT_ALARM) | Wekker óp tijd, ook in Doze |
+| Volledig scherm (USE_FULL_SCREEN_INTENT) | Wekker bovenop het vergrendelscherm |
+| Wakelock + trillen | Scherm aanhouden en trillen tijdens afgaan |
+| Boot (RECEIVE_BOOT_COMPLETED) | Wekkers na herstart opnieuw inplannen |
+| Batterijoptimalisatie uitzonderen | Achtergrondtaken betrouwbaar |
+
+## Architectuur
+
+```
+MainActivity status + instellingen, knop "Nu controleren"
+AlarmActivity volledig-scherm wekker (geluid, trillen, snooze/dismiss)
+App kanalen + dagelijkse 12:00-controle bij opstart
+daily check:
+ AlarmScheduler.scheduleNoonCheck → DailyCheckReceiver (12:00, exact, allowWhileIdle)
+ → ShiftCheck.run agenda morgen ophalen → afronden → wekkers plannen
+ → CalDavClient REPORT calendar-query (time-range, basic auth)
+ → Ical iCal-parse: DTSTART (TZID/UTC), vroegste aanvang
+ → ShiftPlanner afronding op kwartier + Opstaan/Wally/Vertrek
+ring:
+ AlarmScheduler.scheduleAlarm → AlarmRingReceiver (exact, allowWhileIdle)
+ → full-screen intent + directe start → AlarmActivity
+ → Ringer wekkergeluid (loop) + vibratiepatroon
+BootReceiver na reboot: 12:00-controle + geplande wekkers herstellen
+Prefs instellingen/status, versleuteld (EncryptedSharedPreferences)
+```
+
+Details per bestand:
+
+- `core/ShiftPlanner.kt` — pure logica: kwartier-afronding en wekkerplanning
+ (JVM-unit-tests, geen Android-afhankelijkheden).
+- `caldav/Ical.kt` — minimale RFC 5545-parser (VEVENT, DTSTART met TZID/UTC,
+ all-day en geannuleerde events worden overgeslagen).
+- `caldav/CalDavClient.kt` — REPORT calendar-query volgens het recept van de
+ scrapedienst (zie vault-notitie `Homelab/Software ontwikkeling/scrapedienst.md`).
+- `core/ShiftCheck.kt` — orchestrator: ophalen → filteren op morgen → vroegste
+ aanvang → plannen of annuleren; gedeeld door receiver en knop.
+- `core/AlarmScheduler.kt` — exacte alarmen + 12:00-controle + snooze + testwekker.
+- `receivers/DailyCheckReceiver.kt` — 12:00; plant zichzelf via AlarmScheduler
+ steeds opnieuw.
+- `receivers/AlarmRingReceiver.kt` — afgaande wekker: wakelock, full-screen
+ intent (ook vergrendeld scherm), directe activity-start.
+- `receivers/BootReceiver.kt` — herstart-herstel (BOOT_COMPLETED, TIME_SET,
+ TIMEZONE_CHANGED, MY_PACKAGE_REPLACED).
+- `core/Notifier.kt`, `core/Ringer.kt` — meldingen (kanalen "wekkers"/"status")
+ en geluid/trillen.
+
+## Keuzes
+
+- **12:00-controle via exact AlarmManager-alarm**, niet WorkManager: precies om
+ 12:00 (WorkManager periodiek is minimaal 15 min en onnauwkeurig).
+- Bij **meerdere events** op één dag geldt de **vroegste aanvang** als
+ dienstbegintijd.
+- **All-day-events** (bijv. "vrij") worden genegeerd; alleen getimede diensten
+ tellen.
+- Na een fout (bv. offline) blijven eerder geplande wekkers staan en verschijnt
+ een statusmelding.
+- Wachtwoord staat **versleuteld** op het toestel
+ (EncryptedSharedPreferences, AES256-GCM; bij falen plain fallback in de
+ app-sandbox).
+
+## Build
+
+- Gradle wrapper 8.13, AGP 8.7.3, Kotlin 2.0.21
+- compileSdk/targetSdk 34, minSdk 27 (Android 8.1+)
+- Tests: `./gradlew :app:testDebugUnitTest`
+
+## Gerelateerd
+
+- Scrapedienst die de EBS-Diensten-agenda vult:
+ `~/src/scrapedienst` + vault-notitie `scrapedienst.md`
+- CalDAV-referentieclient: `~/src/caldav-mcp`
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..50e4d37
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "nl.eddydevink.buschauffeurswekker"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "nl.eddydevink.buschauffeurswekker"
+ minSdk = 27
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("androidx.security:security-crypto:1.1.0-alpha06")
+
+ testImplementation("junit:junit:4.13.2")
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4828b01
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/nl/eddydevink/buschauffeurswekker/AlarmActivity.kt b/app/src/main/java/nl/eddydevink/buschauffeurswekker/AlarmActivity.kt
new file mode 100644
index 0000000..d710cab
--- /dev/null
+++ b/app/src/main/java/nl/eddydevink/buschauffeurswekker/AlarmActivity.kt
@@ -0,0 +1,109 @@
+package nl.eddydevink.buschauffeurswekker
+
+import android.content.Intent
+import android.media.AudioManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler
+import nl.eddydevink.buschauffeurswekker.core.Notifier
+import nl.eddydevink.buschauffeurswekker.core.Ringer
+import nl.eddydevink.buschauffeurswekker.receivers.AlarmRingReceiver
+import java.time.Instant
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+
+/** Volledig-scherm wekker: geluid, trillen, snooze en wekker uit — ook bovenop het slotscherm. */
+class AlarmActivity : AppCompatActivity() {
+
+ private var ringer: Ringer? = null
+
+ private val clockTick = object : Runnable {
+ override fun run() {
+ updateClock()
+ handler.postDelayed(this, 1000)
+ }
+ }
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_alarm)
+ volumeControlStream = AudioManager.STREAM_ALARM
+
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD,
+ )
+
+ bindUi()
+ startRinging()
+
+ findViewById