fix: turn the screen on when an alarm or timer goes off
SystemUI launches a full-screen intent only when a notification is first posted, never on an update. The alarm service went foreground with a placeholder that had none and added it in the update, so a locked phone could ring behind a black screen. The placeholder now carries it. Timers had no full-screen screen at all. A finished timer now opens TimerRingActivity over the lock screen with Stop and +1 min, posted under a second notification id so the expiry is a fresh post. It only does so while the timer is sounding, so it never covers a ringing alarm's screen or wakes the display for an already auto-silenced timer. Fixes #15
This commit is contained in:
1 parent
2f39b00398
commit
badd7aaa54
12 files changed
+519
-19
No files matched your search
@@ -0,0 +1,134 @@
|
||||
package de.jeanlucmakiola.clockula.ui.ring
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.clockula.domain.TimerState
|
||||
import de.jeanlucmakiola.clockula.domain.timer.TimerRing
|
||||
import de.jeanlucmakiola.clockula.testing.MainDispatcherExtension
|
||||
import de.jeanlucmakiola.clockula.testing.T0
|
||||
import de.jeanlucmakiola.clockula.testing.TimerEngineHarness
|
||||
import de.jeanlucmakiola.clockula.testing.expiredTimer
|
||||
import de.jeanlucmakiola.clockula.testing.runningTimerWith
|
||||
import de.jeanlucmakiola.clockula.testing.timerEngineHarness
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The timer ring screen over the **real** timer engine: which timer it speaks
|
||||
* for, that its buttons are the notification's verbs, and that it closes
|
||||
* whenever the ring ends — including the auto-silence that leaves rows EXPIRED.
|
||||
*/
|
||||
class TimerRingViewModelTest {
|
||||
|
||||
@JvmField
|
||||
@RegisterExtension
|
||||
val main = MainDispatcherExtension()
|
||||
|
||||
/** Subscribed for the whole test, as the visible screen is: the state is shared while observed. */
|
||||
private fun TestScope.viewModel(harness: TimerEngineHarness) = TimerRingViewModel(
|
||||
timers = harness.timers,
|
||||
ringState = harness.ringState,
|
||||
engine = harness.engine,
|
||||
elapsed = harness.elapsed,
|
||||
wall = harness.wallClock,
|
||||
ticker = harness.ticker,
|
||||
).also { viewModel -> backgroundScope.launch { viewModel.state.collect {} } }
|
||||
|
||||
@Test
|
||||
fun `an expired timer rings, with the other expired ones counted`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(expiredTimer(id = 1L, label = "Pasta", sortOrder = 0))
|
||||
harness.given(expiredTimer(id = 2L, label = "Eggs", sortOrder = 1))
|
||||
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value)
|
||||
.isEqualTo(TimerRingUiState.Ringing(timerId = 1L, label = "Pasta", otherExpired = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing expired finishes the screen`(@TempDir tempDir: Path) = runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(runningTimerWith(id = 1L, left = 3.minutes, elapsedNow = harness.elapsed.value))
|
||||
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value).isEqualTo(TimerRingUiState.Finished)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop resets the timer and the screen moves on to the next expired one`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(expiredTimer(id = 1L, label = "Pasta", sortOrder = 0))
|
||||
harness.given(expiredTimer(id = 2L, label = "Eggs", sortOrder = 1))
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.onStop()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.stored(1L)!!.state).isEqualTo(TimerState.IDLE)
|
||||
assertThat(viewModel.state.value)
|
||||
.isEqualTo(TimerRingUiState.Ringing(timerId = 2L, label = "Eggs", otherExpired = 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop leaves a timer that is running again alone`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(expiredTimer(id = 1L))
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
|
||||
// "+1 min" from the shade lands before the screen has re-read the rows.
|
||||
harness.engine.addTime(1L)
|
||||
viewModel.onStop()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.stored(1L)!!.state).isEqualTo(TimerState.RUNNING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `plus one minute runs the timer again and finishes the screen`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(expiredTimer(id = 1L))
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.onAddMinute()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.stored(1L)!!.state).isEqualTo(TimerState.RUNNING)
|
||||
assertThat(viewModel.state.value).isEqualTo(TimerRingUiState.Finished)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an auto-silenced ring finishes the screen though the row stays expired`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
harness.given(expiredTimer(id = 1L))
|
||||
harness.ringState.set(T0)
|
||||
val viewModel = viewModel(harness)
|
||||
advanceUntilIdle()
|
||||
assertThat(viewModel.state.value).isInstanceOf(TimerRingUiState.Ringing::class.java)
|
||||
|
||||
harness.wallClock.advance(TimerRing.AUTO_SILENCE_AFTER + 1.seconds)
|
||||
harness.ticker.tick()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.stored(1L)!!.state).isEqualTo(TimerState.EXPIRED)
|
||||
assertThat(viewModel.state.value).isEqualTo(TimerRingUiState.Finished)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user