med-det-samme/app/src/main/java/no/naiv/meddetsamme/notify/Notifications.kt

135 lines
5.5 KiB
Kotlin
Raw Normal View History

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<String>): 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()
}