fix: close the alarm screen when a restore replaces the ringing alarm
A backup restore stops the ring while its row still exists and deletes the row afterwards, so the screen's one check came too early and left a silent ring screen up for good. It now closes once the coordinator and storage both say nothing rings, which still keeps it up if the import fails and the ring resumes.
This commit is contained in:
@@ -23,8 +23,10 @@ import de.jeanlucmakiola.clockula.domain.time.ZoneProvider
|
||||
import de.jeanlucmakiola.clockula.system.RebootRepair
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
@@ -142,6 +144,14 @@ class AlarmEngine @Inject constructor(
|
||||
/** Restarts a ring the process death or reboot interrupted. Returns the alarm id, or null. */
|
||||
suspend fun resumeRingingIfAny(): Long? = lock.withLock { resumeRingingIfAnyLocked() }
|
||||
|
||||
/**
|
||||
* Whether storage holds a ringing alarm, re-read on every write. The ring
|
||||
* screen watches this as well as [RingCoordinator.ringing]: a backup restore
|
||||
* stops the coordinator first and deletes the ringing row afterwards.
|
||||
*/
|
||||
fun ringStored(): Flow<Boolean> =
|
||||
states.states().map { all -> all.any { it.ringingSince != null } }.distinctUntilChanged()
|
||||
|
||||
/** What the ringing service needs. Null when nothing is ringing. */
|
||||
suspend fun ringSession(): RingSession? = lock.withLock {
|
||||
val state = states.ringing() ?: return@withLock null
|
||||
|
||||
@@ -15,6 +15,7 @@ import de.jeanlucmakiola.clockula.domain.time.WallClock
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import javax.inject.Inject
|
||||
@@ -53,12 +54,13 @@ class AlarmRingViewModel @Inject constructor(
|
||||
)
|
||||
render()
|
||||
}
|
||||
// The ring ending is what closes this screen, whoever ended it — but the
|
||||
// coordinator is seeded null in a fresh process, so storage settles it.
|
||||
// The ring ending is what closes this screen, whoever ended it. Both
|
||||
// sources count: the coordinator is seeded null in a fresh process, and a
|
||||
// backup restore stops it before the ringing row is deleted, so only
|
||||
// the two together say the ring is over.
|
||||
viewModelScope.launch {
|
||||
ring.ringing.collect { alarmId ->
|
||||
if (alarmId == null && engine.ringSession() == null) _state.value = RingUiState.Finished
|
||||
}
|
||||
combine(ring.ringing, engine.ringStored()) { alarmId, stored -> alarmId == null && !stored }
|
||||
.collect { over -> if (over) _state.value = RingUiState.Finished }
|
||||
}
|
||||
// The escape earns itself over time, so the gate needs the clock read
|
||||
// again on a cadence rather than only when the user touches something.
|
||||
|
||||
@@ -227,6 +227,27 @@ class AlarmRingViewModelTest {
|
||||
.isEqualTo(null to RingUiState.Finished)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a backup restore that replaces the ringing alarm finishes the screen`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val engine = engine(tempDir)
|
||||
givenRinging(engine)
|
||||
val viewModel = viewModel(engine)
|
||||
advanceUntilIdle()
|
||||
|
||||
// The restore's order: the ring is stopped while its row still exists,
|
||||
// so the screen must stay up in case the import fails and it resumes.
|
||||
engine.suspendRingForImport()
|
||||
advanceUntilIdle()
|
||||
val whileSuspended = viewModel.state.value
|
||||
// Replacing the rows cascades to the ring state.
|
||||
stateDao.deleteAll()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(whileSuspended).isInstanceOf(RingUiState.Ringing::class.java)
|
||||
assertThat(viewModel.state.value).isEqualTo(RingUiState.Finished)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dismiss does nothing while the maths gate is closed`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
|
||||
Reference in New Issue
Block a user