diff --git a/app/src/main/java/no/naiv/meddetsamme/alarm/AlarmScheduler.kt b/app/src/main/java/no/naiv/meddetsamme/alarm/AlarmScheduler.kt index bc485bc..08fc38a 100644 --- a/app/src/main/java/no/naiv/meddetsamme/alarm/AlarmScheduler.kt +++ b/app/src/main/java/no/naiv/meddetsamme/alarm/AlarmScheduler.kt @@ -90,7 +90,9 @@ class AlarmScheduler(private val context: Context) { // Resume nagging for doses that were unresolved when the device died — // but only PAST-DUE ones. An unresolved log can also be a future dose // (e.g. "Ta nå" + "Angre" before its time); its own occurrence alarm - // handles it when it actually falls due. + // handles it when it actually falls due. Resuming the nag chain is the + // automatic repeat the setting governs, so honour it here too. + if (!no.naiv.meddetsamme.settings.SettingsStore(context).repeatReminders) return val now = System.currentTimeMillis() db.doseLogDao().getUnresolved().forEach { log -> if (log.nagCount < NAG_CAP && log.scheduledAtMillis <= now) { diff --git a/app/src/main/java/no/naiv/meddetsamme/alarm/DoseActionReceiver.kt b/app/src/main/java/no/naiv/meddetsamme/alarm/DoseActionReceiver.kt index 91e3c5c..15c1d90 100644 --- a/app/src/main/java/no/naiv/meddetsamme/alarm/DoseActionReceiver.kt +++ b/app/src/main/java/no/naiv/meddetsamme/alarm/DoseActionReceiver.kt @@ -15,6 +15,7 @@ import no.naiv.meddetsamme.domain.DoseActions * - Taken → TAKEN, decrement inventory by the *logged* amount, stop escalating. * - Snooze → SNOOZED, next nag in 15 min, keeps escalating (and counting) after. * - Skip → SKIPPED, stop escalating, no inventory change. + * - Mute → "Ikke forstyrr": stop escalating, dose stays unresolved. */ class DoseActionReceiver : BroadcastReceiver() { @@ -22,6 +23,7 @@ class DoseActionReceiver : BroadcastReceiver() { const val ACTION_TAKEN = "no.naiv.meddetsamme.action.TAKEN" const val ACTION_SNOOZE = "no.naiv.meddetsamme.action.SNOOZE" const val ACTION_SKIP = "no.naiv.meddetsamme.action.SKIP" + const val ACTION_MUTE = "no.naiv.meddetsamme.action.MUTE" const val EXTRA_LOG_ID = "logId" } @@ -45,6 +47,7 @@ class DoseActionReceiver : BroadcastReceiver() { ACTION_TAKEN -> actions.take(logId) ACTION_SNOOZE -> actions.snooze(logId) ACTION_SKIP -> actions.skip(logId) + ACTION_MUTE -> actions.mute(logId) } } } diff --git a/app/src/main/java/no/naiv/meddetsamme/alarm/DoseAlarmReceiver.kt b/app/src/main/java/no/naiv/meddetsamme/alarm/DoseAlarmReceiver.kt index 9c0e79a..2d68494 100644 --- a/app/src/main/java/no/naiv/meddetsamme/alarm/DoseAlarmReceiver.kt +++ b/app/src/main/java/no/naiv/meddetsamme/alarm/DoseAlarmReceiver.kt @@ -85,7 +85,11 @@ class DoseAlarmReceiver : BroadcastReceiver() { Notifications.doseNotificationId(log.id), Notifications.buildDoseNotification(context, med, log), ) - if (log.nagCount < AlarmScheduler.NAG_CAP) scheduler.armEscalation(log.id) + // Start the automatic re-nag chain only if the user wants repeats; an + // explicit Snooze still re-arms regardless (DoseActions.snooze). + if (repeatReminders(context) && log.nagCount < AlarmScheduler.NAG_CAP) { + scheduler.armEscalation(log.id) + } } private suspend fun escalate(context: Context, logId: Long) { @@ -107,6 +111,14 @@ class DoseAlarmReceiver : BroadcastReceiver() { Notifications.doseNotificationId(logId), Notifications.buildDoseNotification(context, med, nagged), ) - if (nagged.nagCount < AlarmScheduler.NAG_CAP) AlarmScheduler(context).armEscalation(logId) + // Re-arm the next automatic nag only while repeats are enabled. A nag + // already in flight from a Snooze still fires once (this post), then + // stops here — Snooze stays a single deferral, not an endless chain. + if (repeatReminders(context) && nagged.nagCount < AlarmScheduler.NAG_CAP) { + AlarmScheduler(context).armEscalation(logId) + } } + + private fun repeatReminders(context: Context): Boolean = + no.naiv.meddetsamme.settings.SettingsStore(context).repeatReminders } diff --git a/app/src/main/java/no/naiv/meddetsamme/domain/DoseActions.kt b/app/src/main/java/no/naiv/meddetsamme/domain/DoseActions.kt index 7afdc3d..e9a2f68 100644 --- a/app/src/main/java/no/naiv/meddetsamme/domain/DoseActions.kt +++ b/app/src/main/java/no/naiv/meddetsamme/domain/DoseActions.kt @@ -58,6 +58,21 @@ class DoseActions(private val context: Context) { Notifications.cancel(context, Notifications.doseNotificationId(logId)) } + /** + * "Ikke forstyrr": stop nagging this dose but leave it unresolved — the user + * takes responsibility to remember it. Reuses the existing terminal state + * (nagCount at the cap) rather than a new flag, so escalation stops *and* + * armAll() won't resume it after a reboot (it re-arms only nagCount < cap). + * The dose stays PENDING, so it can still be taken from the app later. + */ + suspend fun mute(logId: Long) { + val log = db.doseLogDao().getById(logId) ?: return + if (log.status != DoseStatus.PENDING && log.status != DoseStatus.SNOOZED) return + db.doseLogDao().setNagCount(logId, AlarmScheduler.NAG_CAP) + scheduler.cancelEscalation(logId) + Notifications.cancel(context, Notifications.doseNotificationId(logId)) + } + /** Undo a mis-tap: back to PENDING, restoring stock if it was TAKEN. */ suspend fun undo(logId: Long) { val log = db.doseLogDao().getById(logId) ?: return diff --git a/app/src/main/java/no/naiv/meddetsamme/notify/Notifications.kt b/app/src/main/java/no/naiv/meddetsamme/notify/Notifications.kt index f478aa7..c9e088b 100644 --- a/app/src/main/java/no/naiv/meddetsamme/notify/Notifications.kt +++ b/app/src/main/java/no/naiv/meddetsamme/notify/Notifications.kt @@ -100,6 +100,13 @@ object Notifications { .setOngoing(false) .addAction(0, "Tatt", action(DoseActionReceiver.ACTION_TAKEN)) .addAction(0, "Utsett 15 min", action(DoseActionReceiver.ACTION_SNOOZE)) + // From the second reminder on, offer an escape hatch: stop nagging + // this dose without marking it skipped (the user takes over). + .apply { + if (log.nagCount >= 1) { + addAction(0, "Ikke forstyrr", action(DoseActionReceiver.ACTION_MUTE)) + } + } .build() } diff --git a/app/src/main/java/no/naiv/meddetsamme/settings/SettingsStore.kt b/app/src/main/java/no/naiv/meddetsamme/settings/SettingsStore.kt index a88f8dc..ae1cb1e 100644 --- a/app/src/main/java/no/naiv/meddetsamme/settings/SettingsStore.kt +++ b/app/src/main/java/no/naiv/meddetsamme/settings/SettingsStore.kt @@ -55,6 +55,17 @@ class SettingsStore(context: Context) { get() = prefs.getInt("supply_check_minute", 600) set(v) = prefs.edit { putInt("supply_check_minute", v.coerceIn(0, 1439)) } + /** + * Whether a due dose re-nags automatically every ~10 min until resolved. + * Off → the dose notifies once and goes quiet (no escalation chain). An + * explicit "Utsett 15 min" still re-arms one reminder regardless — that's a + * user request, not the automatic repeat this gates. Default on: dropped + * doses are the failure mode this app exists to prevent. + */ + var repeatReminders: Boolean + get() = prefs.getBoolean("repeat_reminders", true) + set(v) = prefs.edit { putBoolean("repeat_reminders", v) } + /** S3/Garage credentials complete — gates the S3 upload path only. */ val isS3Configured: Boolean get() = !s3Endpoint.isNullOrBlank() && !s3Bucket.isNullOrBlank() && diff --git a/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt b/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt index 3405dd8..1d9dcb8 100644 --- a/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt +++ b/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker @@ -117,6 +118,7 @@ fun SettingsScreen(nav: Nav) { HorizontalDivider() Text("Varsler", style = MaterialTheme.typography.titleMedium) + RepeatRemindersRow(settings) SupplyCheckTimeRow(settings) HorizontalDivider() @@ -153,6 +155,33 @@ fun SettingsScreen(nav: Nav) { } } +/** + * Global toggle for automatic re-nagging. Off → a due dose alerts once and goes + * quiet (an explicit "Utsett 15 min" still gives one more reminder). Takes + * effect for the next dose that fires; no need to touch alarms already armed. + */ +@Composable +private fun RepeatRemindersRow(settings: SettingsStore) { + var enabled by remember { mutableStateOf(settings.repeatReminders) } + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text("Gjenta påminnelser") + Text( + "Mas hvert 10. min til dosen er tatt. Av: ett varsel, så stille.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = enabled, + onCheckedChange = { + enabled = it + settings.repeatReminders = it + }, + ) + } +} + /** Daily low-stock/rx notification time; re-arms the (replacing) alarm on change. */ @OptIn(ExperimentalMaterial3Api::class) @Composable