fix(alarms): open the ring screen when the editor's Delete is refused

Deleting a ringing alarm from the editor is refused, and a refused tap did nothing visible. It now opens the ring screen, the same way out the list's card offers.
This commit is contained in:
makiolaj committed 2026-10-22 08:25:12 +02:00
1 parent 7adda73a79
commit ddec2d8ed1
4 files changed
+31 -2

No files matched your search

@@ -1,5 +1,6 @@
package de.jeanlucmakiola.clockula.ui.alarms
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
@@ -19,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -27,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.clockula.R
import de.jeanlucmakiola.clockula.domain.TimeOfDay
import de.jeanlucmakiola.clockula.ui.common.RingtonePickerScreen
import de.jeanlucmakiola.clockula.ui.ring.AlarmRingActivity
import de.jeanlucmakiola.clockula.ui.common.rememberAlarmTimeFormatter
import de.jeanlucmakiola.clockula.ui.shell.LocalLivePillInset
import de.jeanlucmakiola.floret.components.CollapsingScaffold
@@ -75,6 +78,17 @@ fun AlarmEditorScreen(
if (state is AlarmEditorUiState.Gone) onBack()
}
val context = LocalContext.current
LaunchedEffect(viewModel) {
// Delete is refused while the alarm rings; its ring screen, with the
// dismiss challenge, is the way out, as it is from the list's card.
viewModel.openRingScreen.collect {
context.startActivity(
Intent(context, AlarmRingActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
}
CollapsingScaffold(
title = stringResource(R.string.editor_title),
onBack = onBack,
@@ -18,11 +18,14 @@ import de.jeanlucmakiola.clockula.ui.common.RingtonePicker
import de.jeanlucmakiola.clockula.ui.common.RingtonePickerUiState
import de.jeanlucmakiola.floret.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import java.time.DayOfWeek
import javax.inject.Inject
@@ -62,6 +65,12 @@ class AlarmEditorViewModel @Inject internal constructor(
private val _state = MutableStateFlow<AlarmEditorUiState>(AlarmEditorUiState.Loading)
// Conflated: two refused taps open the ring screen once.
private val ringScreenRequests = Channel<Unit>(Channel.CONFLATED)
/** One-shot: a Delete refused because the alarm is ringing; the screen opens the ring screen. */
val openRingScreen: Flow<Unit> = ringScreenRequests.receiveAsFlow()
val state: StateFlow<AlarmEditorUiState> = _state.asStateFlow()
private val _ringtonePicker = MutableStateFlow<RingtonePickerUiState?>(null)
@@ -224,9 +233,13 @@ class AlarmEditorViewModel @Inject internal constructor(
// A ringing alarm is not deleted from here either: with no
// confirmation in the way, one tap would silence it round its
// dismiss challenge. Its ring screen is the way out.
engine.delete(alarmId, unlessRinging = true)?.let { deleted ->
val deleted = engine.delete(alarmId, unlessRinging = true)
if (deleted != null) {
undo.offer(deleted)
_state.value = AlarmEditorUiState.Gone
} else if (alarms.find(alarmId) != null) {
// Refused because it rings: take the user to where it stops.
ringScreenRequests.trySend(Unit)
}
}
}
@@ -1,6 +1,7 @@
package de.jeanlucmakiola.clockula.ui.alarms
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.clockula.data.alarms.AlarmMapper
import de.jeanlucmakiola.clockula.domain.DismissChallenge
@@ -656,6 +657,7 @@ class AlarmEditorViewModelTest {
assertThat(listOf(harness.ring.ringing.value, harness.alarmDao.stored.map { it.id }))
.isEqualTo(listOf(1L, listOf(1L)))
assertThat(viewModel.state.value).isInstanceOf(AlarmEditorUiState.Editing::class.java)
viewModel.openRingScreen.test { assertThat(awaitItem()).isEqualTo(Unit) }
}
@Test
+1 -1
View File
@@ -1725,7 +1725,7 @@ challenge. The engine re-checks under its lock (`delete(id, unlessRinging = true
so a swipe released just as the alarm fires, before the list has caught up,
deletes nothing and the card slides back. The editor's Delete passes the same
flag: with its confirmation gone, one tap there would otherwise do what the
swipe is kept from doing, so a ringing alarm is dealt with on its ring screen. Each is followed by a snackbar on the list ("07:00 alarm deleted",
swipe is kept from doing, so a refused tap opens the ring screen instead, where the alarm is dealt with. Each is followed by a snackbar on the list ("07:00 alarm deleted",
**Undo**), as in Google Clock. v1 asked before instead, on the argument that a
mistaken delete is found at 07:00 rather than now. But a dialog catches no more
than the snackbar does: both only catch the slip noticed in the moment, and a