Add alert playback controller

This commit is contained in:
ajp_anton
2026-08-16 17:19:52 +00:00
parent 594931e51d
commit de09b283ad
2 changed files with 129 additions and 0 deletions
@@ -0,0 +1,49 @@
package se.ajpanton.notificationsmaster.alerts
/** Plays one resolved effect and invokes [onFinished] when all of its effects finish. */
fun interface AlertEffectPlayer {
fun play(alert: QueuedAlert, onFinished: () -> Unit)
fun stop() {}
}
/** Serializes effect playback around the pure queue policy. */
class AlertPlaybackController(
private val effectPlayer: AlertEffectPlayer,
private val settings: () -> AlertQueueSettings,
) {
private var queue = AlertQueue()
@Synchronized
fun enqueue(alert: QueuedAlert): AlertQueueTransition {
val before = queue
return AlertQueueController.enqueue(queue, alert, settings()).also { transition ->
queue = transition.queue
playCurrentIfChanged(before.current)
}
}
@Synchronized
fun currentQueue() = queue
@Synchronized
fun stop() {
queue = AlertQueue()
effectPlayer.stop()
}
@Synchronized
private fun complete(alert: QueuedAlert) {
if (queue.current != alert) return
val before = queue
queue = AlertQueueController.completeCurrent(queue).queue
playCurrentIfChanged(before.current)
}
private fun playCurrentIfChanged(previous: QueuedAlert?) {
val current = queue.current
if (previous == current) return
effectPlayer.stop()
current?.let { alert -> effectPlayer.play(alert) { complete(alert) } }
}
}
@@ -0,0 +1,80 @@
package se.ajpanton.notificationsmaster.alerts
import org.junit.Assert.assertEquals
import org.junit.Test
class AlertPlaybackControllerTest {
private val effects = FakeEffects()
private val controller = AlertPlaybackController(effects) { AlertQueueSettings() }
@Test
fun replacementStopsCurrentAndStartsIncomingAlert() {
val first = alert("first")
val second = alert("second")
controller.enqueue(first)
controller.enqueue(second)
assertEquals(listOf("first", "second"), effects.played)
assertEquals(2, effects.stopCalls)
}
@Test
fun protectedCurrentContinuesWhileIncomingAlertWaits() {
val current = alert("current", protected = true)
val incoming = alert("incoming")
controller.enqueue(current)
controller.enqueue(incoming)
assertEquals(listOf("current"), effects.played)
assertEquals(listOf("current", "incoming"), controller.currentQueue().alerts.map { it.ruleId })
}
@Test
fun completionStartsTheNextWaitingAlert() {
val current = alert("current", protected = true)
val incoming = alert("incoming")
controller.enqueue(current)
controller.enqueue(incoming)
effects.finish("current")
assertEquals(listOf("current", "incoming"), effects.played)
assertEquals(listOf("incoming"), controller.currentQueue().alerts.map { it.ruleId })
}
@Test
fun staleCompletionCannotAdvanceReplacement() {
val first = alert("first")
controller.enqueue(first)
val staleCompletion = effects.callbacks.getValue("first")
controller.enqueue(alert("second"))
staleCompletion()
assertEquals(listOf("second"), controller.currentQueue().alerts.map { it.ruleId })
}
private fun alert(id: String, protected: Boolean = false) = QueuedAlert(
id,
AlertSnapshot("content://" + id, emptyList(), protected, false),
)
private class FakeEffects : AlertEffectPlayer {
val played = mutableListOf<String>()
val callbacks = mutableMapOf<String, () -> Unit>()
var stopCalls = 0
override fun play(alert: QueuedAlert, onFinished: () -> Unit) {
played += alert.ruleId
callbacks[alert.ruleId] = onFinished
}
override fun stop() {
stopCalls++
}
fun finish(ruleId: String) = callbacks.getValue(ruleId).invoke()
}
}