diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScreen.kt index 1b3fa7a..82e143c 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScreen.kt @@ -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, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt index b88ca71..0b2ba8b 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt @@ -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.Loading) + // Conflated: two refused taps open the ring screen once. + private val ringScreenRequests = Channel(Channel.CONFLATED) + + /** One-shot: a Delete refused because the alarm is ringing; the screen opens the ring screen. */ + val openRingScreen: Flow = ringScreenRequests.receiveAsFlow() + val state: StateFlow = _state.asStateFlow() private val _ringtonePicker = MutableStateFlow(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) } } } diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModelTest.kt index 850b677..219793a 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModelTest.kt @@ -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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9d1a60d..5f7b50b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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