Add custom alert queue controller
This commit is contained in:
@@ -150,6 +150,22 @@ data class AlertDecision(
|
|||||||
val allowDuringDnd: Boolean,
|
val allowDuringDnd: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Resolved at match time so queued alerts never point at mutable settings. */
|
||||||
|
data class AlertSnapshot(
|
||||||
|
val soundUri: String?,
|
||||||
|
val vibrationPattern: List<Long>,
|
||||||
|
val playToCompletion: Boolean,
|
||||||
|
val allowDuringDnd: Boolean,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(soundUri != null || vibrationPattern.any { it > 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AlertDecision.snapshot(): AlertSnapshot? = profile?.let {
|
||||||
|
AlertSnapshot(it.soundUri, it.vibrationPattern, playToCompletion, allowDuringDnd)
|
||||||
|
}
|
||||||
|
|
||||||
data class AlertEvaluation(
|
data class AlertEvaluation(
|
||||||
val decisions: List<AlertDecision>,
|
val decisions: List<AlertDecision>,
|
||||||
val silenceUnmatchedDirectAlert: Boolean,
|
val silenceUnmatchedDirectAlert: Boolean,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.alerts
|
||||||
|
|
||||||
|
data class QueuedAlert(
|
||||||
|
val ruleId: String,
|
||||||
|
val snapshot: AlertSnapshot,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The first alert is playing; any following alerts are waiting. */
|
||||||
|
data class AlertQueue(
|
||||||
|
val alerts: List<QueuedAlert> = emptyList(),
|
||||||
|
) {
|
||||||
|
val current get() = alerts.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AlertQueueTransition(
|
||||||
|
val queue: AlertQueue,
|
||||||
|
val started: QueuedAlert? = null,
|
||||||
|
val interrupted: QueuedAlert? = null,
|
||||||
|
val dropped: QueuedAlert? = null,
|
||||||
|
val completed: QueuedAlert? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
object AlertQueueController {
|
||||||
|
fun enqueue(
|
||||||
|
queue: AlertQueue,
|
||||||
|
incoming: QueuedAlert,
|
||||||
|
settings: AlertQueueSettings,
|
||||||
|
): AlertQueueTransition {
|
||||||
|
val alerts = queue.alerts
|
||||||
|
if (alerts.isEmpty()) return AlertQueueTransition(AlertQueue(listOf(incoming)), started = incoming)
|
||||||
|
|
||||||
|
val tail = alerts.last()
|
||||||
|
val appended = if (tail.snapshot.playToCompletion) alerts + incoming else alerts.dropLast(1) + incoming
|
||||||
|
val interrupted = tail.takeUnless { tail.snapshot.playToCompletion }
|
||||||
|
if (appended.size <= settings.maximumLength) {
|
||||||
|
return AlertQueueTransition(
|
||||||
|
AlertQueue(appended),
|
||||||
|
started = incoming.takeIf { tail == alerts.first() && interrupted != null },
|
||||||
|
interrupted = interrupted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return when (settings.overflow) {
|
||||||
|
AlertQueueOverflow.DROP_NEWEST -> AlertQueueTransition(queue, dropped = incoming)
|
||||||
|
AlertQueueOverflow.DROP_OLDEST -> {
|
||||||
|
val next = appended.drop(1)
|
||||||
|
AlertQueueTransition(
|
||||||
|
AlertQueue(next),
|
||||||
|
started = next.first(),
|
||||||
|
interrupted = alerts.first(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completeCurrent(queue: AlertQueue): AlertQueueTransition {
|
||||||
|
val current = queue.current ?: return AlertQueueTransition(queue)
|
||||||
|
val next = queue.alerts.drop(1)
|
||||||
|
return AlertQueueTransition(AlertQueue(next), started = next.firstOrNull(), completed = current)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.alerts
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AlertQueueControllerTest {
|
||||||
|
private val normal = alert("normal")
|
||||||
|
private val protected = alert("protected", protected = true)
|
||||||
|
private val settings = AlertQueueSettings()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startsImmediatelyWhenQueueIsEmpty() {
|
||||||
|
val result = AlertQueueController.enqueue(AlertQueue(), normal, settings)
|
||||||
|
|
||||||
|
assertEquals(listOf(normal), result.queue.alerts)
|
||||||
|
assertEquals(normal, result.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun replacesAndInterruptsAnInterruptibleCurrentAlert() {
|
||||||
|
val result = AlertQueueController.enqueue(AlertQueue(listOf(normal)), alert("next"), settings)
|
||||||
|
|
||||||
|
assertEquals(listOf("next"), result.queue.alerts.map { it.ruleId })
|
||||||
|
assertEquals(normal, result.interrupted)
|
||||||
|
assertEquals("next", result.started?.ruleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun keepsProtectedCurrentAlertAndReplacesOnlyInterruptibleTail() {
|
||||||
|
val first = protected
|
||||||
|
val replaced = alert("replaced")
|
||||||
|
val result = AlertQueueController.enqueue(AlertQueue(listOf(first, normal)), replaced, settings)
|
||||||
|
|
||||||
|
assertEquals(listOf("protected", "replaced"), result.queue.alerts.map { it.ruleId })
|
||||||
|
assertEquals(normal, result.interrupted)
|
||||||
|
assertNull(result.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun droppingOldestInterruptsCurrentAndStartsNextAlert() {
|
||||||
|
val first = protected
|
||||||
|
val second = alert("second", protected = true)
|
||||||
|
val third = alert("third", protected = true)
|
||||||
|
val incoming = alert("incoming")
|
||||||
|
val queue = AlertQueue(listOf(first, second, third))
|
||||||
|
|
||||||
|
val result = AlertQueueController.enqueue(queue, incoming, settings)
|
||||||
|
|
||||||
|
assertEquals(listOf("second", "third", "incoming"), result.queue.alerts.map { it.ruleId })
|
||||||
|
assertEquals(first, result.interrupted)
|
||||||
|
assertEquals(second, result.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun droppingNewestKeepsExistingQueue() {
|
||||||
|
val queue = AlertQueue(listOf(protected))
|
||||||
|
val incoming = alert("incoming")
|
||||||
|
val result = AlertQueueController.enqueue(
|
||||||
|
queue,
|
||||||
|
incoming,
|
||||||
|
AlertQueueSettings(1, AlertQueueOverflow.DROP_NEWEST),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(queue, result.queue)
|
||||||
|
assertEquals(incoming, result.dropped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun completingCurrentStartsNextWaitingAlert() {
|
||||||
|
val next = alert("next")
|
||||||
|
val result = AlertQueueController.completeCurrent(AlertQueue(listOf(protected, next)))
|
||||||
|
|
||||||
|
assertEquals(listOf(next), result.queue.alerts)
|
||||||
|
assertEquals(protected, result.completed)
|
||||||
|
assertEquals(next, result.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snapshotDoesNotPointAtTheOriginalProfile() {
|
||||||
|
val profile = AlertProfile("profile", "Original", "content://original")
|
||||||
|
val decision = AlertDecision("rule", AlertOutcome.PLAY_PROFILE, profile, false, false)
|
||||||
|
|
||||||
|
assertEquals("content://original", decision.snapshot()?.soundUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun alert(id: String, protected: Boolean = false) = QueuedAlert(
|
||||||
|
id,
|
||||||
|
AlertSnapshot("content://" + id, emptyList(), protected, false),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user