package no.naiv.meddetsamme.notify import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import no.naiv.meddetsamme.MainActivity import no.naiv.meddetsamme.R import no.naiv.meddetsamme.alarm.DoseActionReceiver import no.naiv.meddetsamme.data.DoseLog import no.naiv.meddetsamme.data.Medication import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter object Notifications { const val CHANNEL_DOSES = "doses" const val CHANNEL_SUPPLY = "supply" /** Stable per-occurrence notification id: every re-nag *replaces* the card. */ fun doseNotificationId(logId: Long): Int = (logId % Int.MAX_VALUE).toInt() const val SUPPLY_NOTIFICATION_ID = 1_000_000_007 private val timeFmt = DateTimeFormatter.ofPattern("HH:mm") fun createChannels(context: Context) { val nm = context.getSystemService(NotificationManager::class.java) nm.createNotificationChannel( NotificationChannel( CHANNEL_DOSES, "Doser", // HIGH: heads-up + sound. The entire point of the app. NotificationManager.IMPORTANCE_HIGH, ).apply { description = "Påminnelser om å ta medisin" setBypassDnd(false) }, ) nm.createNotificationChannel( NotificationChannel( CHANNEL_SUPPLY, "Lager og resept", NotificationManager.IMPORTANCE_DEFAULT, ).apply { description = "Lite på lager eller resept som må fornyes" }, ) } /** * The dose reminder card. [nagCount] > 0 marks a re-nag — same id, so it * replaces the previous card but re-alerts (no setOnlyAlertOnce: re-alerting * is the escalation). */ fun buildDoseNotification(context: Context, med: Medication, log: DoseLog): Notification { val scheduled = Instant.ofEpochMilli(log.scheduledAtMillis).atZone(ZoneId.systemDefault()) val title = "${med.name} ${med.strength}".trim() val body = buildString { append("${formatAmount(log.amount)} ${med.unit} kl. ${timeFmt.format(scheduled)}") if (med.withFood) append(" — tas med mat") if (log.nagCount > 0) append(" (påminnelse ${log.nagCount})") } fun action(action: String): PendingIntent = PendingIntent.getBroadcast( context, // Distinct requestCode per (log, action); same (log, action) replaces. (log.id * 10 + action.hashCode().mod(10)).toInt(), Intent(context, DoseActionReceiver::class.java) .setAction(action) .putExtra(DoseActionReceiver.EXTRA_LOG_ID, log.id), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) val contentIntent = PendingIntent.getActivity( context, doseNotificationId(log.id), Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) return NotificationCompat.Builder(context, CHANNEL_DOSES) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(body) .setCategory(NotificationCompat.CATEGORY_REMINDER) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(contentIntent) .setAutoCancel(false) .setOngoing(false) .addAction(0, "Tatt", action(DoseActionReceiver.ACTION_TAKEN)) .addAction(0, "Utsett 15 min", action(DoseActionReceiver.ACTION_SNOOZE)) .build() } fun buildSupplyNotification(context: Context, lines: List): Notification { val contentIntent = PendingIntent.getActivity( context, SUPPLY_NOTIFICATION_ID, Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) return NotificationCompat.Builder(context, CHANNEL_SUPPLY) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(if (lines.size == 1) lines.first() else "Apotek-ærend (${lines.size})") .setContentText(lines.joinToString(" · ")) .setStyle(NotificationCompat.InboxStyle().also { s -> lines.forEach(s::addLine) }) .setCategory(NotificationCompat.CATEGORY_REMINDER) .setContentIntent(contentIntent) .setAutoCancel(true) .build() } /** Post if allowed; silently skipping is fine — channel-level blocks are user intent. */ fun post(context: Context, id: Int, notification: Notification) { val nm = NotificationManagerCompat.from(context) if (nm.areNotificationsEnabled()) { try { nm.notify(id, notification) } catch (_: SecurityException) { // POST_NOTIFICATIONS revoked between check and post; nothing to do. } } } fun cancel(context: Context, id: Int) = NotificationManagerCompat.from(context).cancel(id) private fun formatAmount(amount: Double): String = if (amount == amount.toLong().toDouble()) amount.toLong().toString() else amount.toString() }