From 6c0fa97e508622d364dc32d34865c13bf309ea41 Mon Sep 17 00:00:00 2001 From: eddy Date: Wed, 14 Oct 2026 20:19:23 +0200 Subject: [PATCH] Hoofdscherm omgebouwd naar wekker-UI: klok, geplande wekkers, dienstinfo; testwekker verplaatst uit de UI naar launch-intent --- README.md | 15 +-- .../buschauffeurswekker/MainActivity.kt | 91 +++++++++++----- .../buschauffeurswekker/core/Prefs.kt | 6 ++ .../buschauffeurswekker/core/ShiftCheck.kt | 2 + app/src/main/res/layout/activity_main.xml | 100 +++++++++++------- 5 files changed, 145 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 2baada3..f460b10 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,14 @@ automatisch zet op basis van de Nextcloud CalDAV-agenda **EBS-Diensten**. 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: +3. Open de app → **Instellingen** → vul 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. +4. **Opslaan**, terug op het hoofdscherm **Nu controleren** om te testen. +5. Verleen de meldingenmachtiging bij de eerste start (systeemdialoog). ## Machtigingen @@ -52,7 +50,8 @@ automatisch zet op basis van de Nextcloud CalDAV-agenda **EBS-Diensten**. ## Architectuur ``` -MainActivity status + instellingen, knop "Nu controleren" +MainActivity hoofdscherm: status + "Nu controleren" + "Wekker testen" + link naar instellingen +SettingsActivity instellingen: CalDAV-url, gebruiker, app-wachtwoord (+ uitleg) AlarmActivity volledig-scherm wekker (geluid, trillen, snooze/dismiss) App kanalen + dagelijkse 12:00-controle bij opstart daily check: @@ -69,6 +68,10 @@ BootReceiver na reboot: 12:00-controle + geplande wekkers herstellen Prefs instellingen/status, versleuteld (EncryptedSharedPreferences) ``` +Hoofdscherm en instellingen zijn bewust gescheiden; installatiehulp via +launch-intent-extra's (`--es url/user/pass`) slaat de config op en draait +direct een controle, zonder UI-interactie. + Details per bestand: - `core/ShiftPlanner.kt` — pure logica: kwartier-afronding en wekkerplanning diff --git a/app/src/main/java/nl/eddydevink/buschauffeurswekker/MainActivity.kt b/app/src/main/java/nl/eddydevink/buschauffeurswekker/MainActivity.kt index 6485b14..9d4e98a 100644 --- a/app/src/main/java/nl/eddydevink/buschauffeurswekker/MainActivity.kt +++ b/app/src/main/java/nl/eddydevink/buschauffeurswekker/MainActivity.kt @@ -5,6 +5,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity @@ -15,14 +17,27 @@ import kotlinx.coroutines.launch import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler import nl.eddydevink.buschauffeurswekker.core.Prefs import nl.eddydevink.buschauffeurswekker.core.ShiftCheck -import java.time.Instant -import java.time.ZoneId +import nl.eddydevink.buschauffeurswekker.core.ShiftPlanner +import java.time.LocalDateTime +import java.time.LocalTime import java.time.format.DateTimeFormatter -/** Hoofdscherm: status + acties. Instellingen staan in [SettingsActivity]. */ +/** + * Hoofdscherm: klok + de geplande wekkers van morgen. Instellingen zitten in + * [SettingsActivity]; de testwekker is alleen bereikbaar via launch-intent + * (--ez test true), niet via de productie-UI. + */ class MainActivity : AppCompatActivity() { private lateinit var prefs: Prefs + private val handler = Handler(Looper.getMainLooper()) + + private val clockTick = object : Runnable { + override fun run() { + updateClock() + handler.postDelayed(this, 1000) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,32 +47,37 @@ class MainActivity : AppCompatActivity() { requestNotificationPermission() findViewById(R.id.btn_check).setOnClickListener { runCheck() } - findViewById(R.id.btn_test).setOnClickListener { - AlarmScheduler.scheduleTest(this) - toast("Testwekker gaat over 10 seconden af") - } findViewById(R.id.btn_settings).setOnClickListener { startActivity(Intent(this, SettingsActivity::class.java)) } AlarmScheduler.scheduleNoonCheck(this) - // Config via launch-intent (installatiehulp): - // adb shell am start ... --es url "..." --es user "..." --es pass "..." + // Installatiehulp/config zonder UI-interactie: + // --es url "…" --es user "…" --es pass "…" → config opslaan + controleren + // --ez test true → testwekker (debugging) handleSetupIntent(intent) + if (intent?.getBooleanExtra(EXTRA_TEST, false) == true) { + AlarmScheduler.scheduleTest(this) + toast("Testwekker gaat over 10 seconden af") + } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) handleSetupIntent(intent) + if (intent.getBooleanExtra(EXTRA_TEST, false)) { + AlarmScheduler.scheduleTest(this) + toast("Testwekker gaat over 10 seconden af") + } } /** Slaat CalDAV-config van intent-extra's op en draait direct een controle. */ - private fun handleSetupIntent(intent: Intent?) { - val url = intent?.getStringExtra(EXTRA_SETUP_URL) - val user = intent?.getStringExtra(EXTRA_SETUP_USER) - val pass = intent?.getStringExtra(EXTRA_SETUP_PASS) + private fun handleSetupIntent(intent: Intent) { + val url = intent.getStringExtra(EXTRA_SETUP_URL) + val user = intent.getStringExtra(EXTRA_SETUP_USER) + val pass = intent.getStringExtra(EXTRA_SETUP_PASS) if (url == null && user == null && pass == null) return if (!url.isNullOrBlank()) prefs.calendarUrl = url.trim() if (!user.isNullOrBlank()) prefs.username = user.trim() @@ -72,24 +92,40 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() renderStatus() + updateClock() + handler.removeCallbacks(clockTick) + handler.post(clockTick) + } + + override fun onPause() { + handler.removeCallbacks(clockTick) + super.onPause() + } + + private fun updateClock() { + val now = LocalTime.now() + findViewById(R.id.tv_clock).text = HH_MM.format(now) + findViewById(R.id.tv_date).text = DATE.format(java.time.LocalDate.now()) } private fun renderStatus() { val p = prefs - val last = p.lastCheckedAtMillis.takeIf { it > 0 }?.let { - Instant.ofEpochMilli(it) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm")) - } ?: "nooit" - findViewById(R.id.tv_status).text = buildString { - append("Dagelijkse controle: elke dag om 12:00\n") - append("Laatst gecontroleerd: $last\n\n") - append(p.lastStatusTxt ?: "Nog niet gecontroleerd.") - val (day, alarms) = p.loadPlan() - if (day != null && alarms.isNotEmpty()) { - append("\n\nGepland voor $day:") - alarms.forEach { append("\n ${it.name} om ${it.time.toLocalTime()}") } + val (_, alarms) = p.loadPlan() + val next = findViewById(R.id.tv_next_alarms) + val info = findViewById(R.id.tv_shift_info) + + if (alarms.isNotEmpty()) { + next.text = alarms.joinToString("\n") { "${it.name} ${HH_MM.format(it.time)}" } + val shift = p.lastShiftStartIso?.let { runCatching { LocalDateTime.parse(it) }.getOrNull() } + info.text = if (shift != null) { + val rounded = ShiftPlanner.roundDownToQuarter(shift.toLocalTime()) + "Dienst om ${HH_MM.format(shift.toLocalTime())} · afgerond ${HH_MM.format(rounded)}" + } else { + p.lastStatusTxt.orEmpty() } + } else { + next.text = "Geen wekkers" + info.text = p.lastStatusTxt ?: "Nog niet gecontroleerd" } } @@ -125,5 +161,8 @@ class MainActivity : AppCompatActivity() { const val EXTRA_SETUP_URL = "url" const val EXTRA_SETUP_USER = "user" const val EXTRA_SETUP_PASS = "pass" + const val EXTRA_TEST = "test" + val HH_MM = DateTimeFormatter.ofPattern("HH:mm") + val DATE = DateTimeFormatter.ofPattern("EEEE d MMMM") } } \ No newline at end of file diff --git a/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/Prefs.kt b/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/Prefs.kt index 98f59fe..48e4264 100644 --- a/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/Prefs.kt +++ b/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/Prefs.kt @@ -61,6 +61,11 @@ class Prefs(context: Context) { get() = sp.getString(KEY_NOON_RUN, null) set(value) = sp.edit().putString(KEY_NOON_RUN, value).apply() + /** Laatste gevonden dienstaanvang (ISO LocalDateTime) — voor de weergave op het hoofdscherm. */ + var lastShiftStartIso: String? + get() = sp.getString(KEY_SHIFT_START, null) + set(value) = sp.edit().putString(KEY_SHIFT_START, value).apply() + private var plannedDayIso: String? get() = sp.getString(KEY_PLAN_DAY, null) set(value) = sp.edit().putString(KEY_PLAN_DAY, value).apply() @@ -114,5 +119,6 @@ class Prefs(context: Context) { const val KEY_NOON_RUN = "last_noon_run" const val KEY_PLAN_DAY = "plan_day" const val KEY_PLAN_ALARMS = "plan_alarms" + const val KEY_SHIFT_START = "shift_start" } } \ No newline at end of file diff --git a/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/ShiftCheck.kt b/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/ShiftCheck.kt index 0855f93..5e7a1d8 100644 --- a/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/ShiftCheck.kt +++ b/app/src/main/java/nl/eddydevink/buschauffeurswekker/core/ShiftCheck.kt @@ -55,6 +55,7 @@ object ShiftCheck { if (starts.isEmpty()) { AlarmScheduler.cancelAll(context) prefs.clearPlan() + prefs.lastShiftStartIso = null val msg = "Morgen ($tomorrow) geen dienst — er zijn geen wekkers gezet" prefs.lastStatusTxt = msg prefs.lastCheckedAtMillis = System.currentTimeMillis() @@ -63,6 +64,7 @@ object ShiftCheck { } val shiftStart = starts.minOrNull()!! + prefs.lastShiftStartIso = shiftStart.toString() val plan = ShiftPlanner.plan(shiftStart) plan.forEach { AlarmScheduler.scheduleAlarm(context, it) } prefs.storePlan(tomorrow, plan) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f205bba..bda9b1a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,44 +1,70 @@ - + android:gravity="center_horizontal" + android:orientation="vertical" + android:padding="24dp"> - + + + + + + + + + + + + + android:layout_marginTop="40dp" + android:text="Nu controleren" /> - - - - - - - - - \ No newline at end of file + + \ No newline at end of file