test(playback): pure sleep-timer math + controller uses it

Extracts deadline/remaining/expiry into SleepTimer with unit tests (arm,
disarm, countdown clamp, expiry) so the M10 sleep timer is directly covered.
This commit is contained in:
John Smith
2026-09-12 19:44:18 -04:00
parent 989bc4ca56
commit b88ed0c518
3 changed files with 62 additions and 4 deletions
@@ -281,9 +281,9 @@ class PlaybackController(
cancelSleepTimer()
return
}
sleepDeadlineElapsedMs = android.os.SystemClock.elapsedRealtime() + minutes * 60_000L
sleepDeadlineElapsedMs = SleepTimer.deadlineMs(android.os.SystemClock.elapsedRealtime(), minutes)
sleepActive = true
_sleepRemainingMs.value = minutes * 60_000L
_sleepRemainingMs.value = SleepTimer.remainingMs(sleepDeadlineElapsedMs, android.os.SystemClock.elapsedRealtime())
}
fun cancelSleepTimer() {
@@ -294,9 +294,9 @@ class PlaybackController(
private fun tickSleepTimer(player: MediaController) {
if (!sleepActive) return
val remaining = (sleepDeadlineElapsedMs - android.os.SystemClock.elapsedRealtime()).coerceAtLeast(0L)
val remaining = SleepTimer.remainingMs(sleepDeadlineElapsedMs, android.os.SystemClock.elapsedRealtime())
_sleepRemainingMs.value = remaining
if (remaining > 0L) return
if (!SleepTimer.expired(sleepDeadlineElapsedMs, android.os.SystemClock.elapsedRealtime())) return
sleepActive = false
// Fade out over a short window, then stop; restore volume for the
// next manual play.
@@ -0,0 +1,22 @@
package com.heretek.dorado_hd.media
/**
* Pure sleep-timer math (post-device extension, canon §10). Kept free of
* Android so arming/expiry is unit-testable.
*/
object SleepTimer {
/** Deadline for [minutes] from [elapsedNowMs]; 0 or less means disarmed. */
fun deadlineMs(elapsedNowMs: Long, minutes: Int): Long =
if (minutes <= 0) 0L else elapsedNowMs + minutes * 60_000L
/** Milliseconds left, never negative. A disarmed deadline yields 0. */
fun remainingMs(deadlineMs: Long, elapsedNowMs: Long): Long {
if (deadlineMs <= 0L) return 0L
return (deadlineMs - elapsedNowMs).coerceAtLeast(0L)
}
/** True once the deadline has passed (disarmed deadlines never expire). */
fun expired(deadlineMs: Long, elapsedNowMs: Long): Boolean =
deadlineMs > 0L && elapsedNowMs >= deadlineMs
}
@@ -0,0 +1,36 @@
package com.heretek.dorado_hd
import com.heretek.dorado_hd.media.SleepTimer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class SleepTimerTest {
@Test
fun `minutes arm a deadline and non-positive values disarm`() {
assertEquals(30 * 60_000L, SleepTimer.deadlineMs(0L, 30))
assertEquals(1_000L + 15 * 60_000L, SleepTimer.deadlineMs(1_000L, 15))
assertEquals(0L, SleepTimer.deadlineMs(1_000L, 0))
assertEquals(0L, SleepTimer.deadlineMs(1_000L, -5))
}
@Test
fun `remaining counts down and never goes negative`() {
val deadline = SleepTimer.deadlineMs(0L, 2)
assertEquals(120_000L, SleepTimer.remainingMs(deadline, 0L))
assertEquals(60_000L, SleepTimer.remainingMs(deadline, 60_000L))
assertEquals(0L, SleepTimer.remainingMs(deadline, 200_000L))
assertEquals(0L, SleepTimer.remainingMs(0L, 5L))
}
@Test
fun `expiry fires at the deadline and never when disarmed`() {
val deadline = SleepTimer.deadlineMs(10_000L, 1)
assertFalse(SleepTimer.expired(deadline, 69_999L))
assertTrue(SleepTimer.expired(deadline, 70_000L))
assertTrue(SleepTimer.expired(deadline, 99_000L))
assertFalse(SleepTimer.expired(0L, Long.MAX_VALUE))
}
}