diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackController.kt b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackController.kt new file mode 100644 index 0000000..e7aaafe --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackController.kt @@ -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) } } + } +} diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackControllerTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackControllerTest.kt new file mode 100644 index 0000000..4a7eae4 --- /dev/null +++ b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertPlaybackControllerTest.kt @@ -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() + val callbacks = mutableMapOf 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() + } +}