Reminder subsystem: exact alarms, escalation, boot re-arm — verified on emulator
Milestone 4. AlarmScheduler is the only AlarmManager writer; one stable PendingIntent requestCode per dose-time means re-arming replaces and can never stack (the no-drift invariant is OS-enforced). DoseAlarmReceiver handles both occurrence and escalation alarms idempotently and always re-arms the next occurrence before anything else so the chain can't break. nagCount lives in dose_log so the ~6-nag cap survives reboot. SCHEDULE_EXACT_ALARM added maxSdk 32 (USE_EXACT_ALARM is 33+ only). Emulator-verified (API 35): exact alarm armed (window=0, policy_permission), fired on time, HIGH notification with Tatt/Utsett, escalation armed +10 min, next day re-armed, Tatt → TAKEN + inventory 10→9 + escalation cancelled, reboot → BootReceiver re-armed everything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
63f1ecd9fd
commit
46a5d7e98e
16 changed files with 714 additions and 14 deletions
|
|
@ -0,0 +1,59 @@
|
|||
package no.naiv.meddetsamme.alarm
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.time.LocalDate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import no.naiv.meddetsamme.data.MedDatabase
|
||||
import no.naiv.meddetsamme.domain.ScheduleEngine
|
||||
import no.naiv.meddetsamme.notify.Notifications
|
||||
|
||||
/**
|
||||
* Daily derived-state check: low stock (inventory ÷ scheduled rate) and
|
||||
* prescription renewal (rx expiry, independent of stock — decision #9).
|
||||
* One digest notification, not one per med.
|
||||
*/
|
||||
class SupplyCheckReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val pending = goAsync()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
check(context)
|
||||
} finally {
|
||||
AlarmScheduler(context).armSupplyCheck() // chain the next daily check
|
||||
pending.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun check(context: Context) {
|
||||
val db = MedDatabase.get(context)
|
||||
val today = LocalDate.now()
|
||||
val lines = mutableListOf<String>()
|
||||
|
||||
for (med in db.medicationDao().getActive()) {
|
||||
val doseTimes = db.doseTimeDao().getForMed(med.id)
|
||||
val rate = ScheduleEngine.dailyConsumption(doseTimes)
|
||||
if (ScheduleEngine.needsRefill(med.inventoryUnits, rate, med.lowStockLeadDays)) {
|
||||
val days = ScheduleEngine.daysOfSupply(med.inventoryUnits, rate)
|
||||
lines += "${med.name}: lager for ${days?.toInt() ?: 0} dager"
|
||||
}
|
||||
if (ScheduleEngine.needsRenewal(med.rxExpiryEpochDay, today)) {
|
||||
val expired = med.rxExpiryEpochDay!! < today.toEpochDay()
|
||||
lines += if (expired) "${med.name}: resept utløpt" else "${med.name}: resept må fornyes"
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.isNotEmpty()) {
|
||||
Notifications.post(
|
||||
context,
|
||||
Notifications.SUPPLY_NOTIFICATION_ID,
|
||||
Notifications.buildSupplyNotification(context, lines),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue