From 1df6c3d5d9dc63d0ac12a5a3f8e81ba49eb7ac88 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 15:50:33 +0100 Subject: [PATCH 01/17] - Allowing Preview of Shared screens. --- shared/build.gradle.kts | 8 +++++++ .../home/home/SharedSleepScreenPreview.kt | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 16e35f95f..9a55b0ecd 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.implementation + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -58,8 +60,11 @@ kotlin { implementation(libs.kotlinx.coroutines.test) } androidMain.dependencies { + implementation(project.dependencies.platform(libs.androidx.compose.bom)) implementation(libs.jts.core) implementation(libs.ktor.client.okhttp) + implementation(libs.ui.tooling.preview) + implementation(libs.ui.tooling) } iosMain.dependencies { implementation(libs.ktor.client.darwin) @@ -77,6 +82,9 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + buildFeatures { + compose = true + } // Reuse shared resources for the Android target so JSON data files live // in a single canonical location consumed by both platforms. sourceSets["main"].resources.srcDir("src/commonMain/resources") diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt new file mode 100644 index 000000000..8d4206451 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -0,0 +1,22 @@ +package org.scottishtecharmy.soundscape.screens.home.home + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview(showBackground = true) +@Composable +fun SharedSleepScreenPreview() { + SharedSleepScreen( + onWakeUp = {}, + onExit = {} + ) +} + +@Preview(showBackground = true) +@Composable +fun WakeButtonPreview() { + WakeButton( + text = "Wake On Leave", + onClick = { }, + ) +} \ No newline at end of file From 041c941975e80bbdca9d781fc275932fb4f7c849 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 15:50:55 +0100 Subject: [PATCH 02/17] - Implementing WakeButton to be used on Sleep Screen --- .../composeResources/values/strings.xml | 2 + .../screens/home/home/SharedSleepScreen.kt | 52 +++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 7a4f61c47..f45196d45 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -342,6 +342,8 @@ put Soundscape to sleep Wake Up Now + + Wake On Leave Approaching intersection diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 12fdc595d..52ade6644 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -23,6 +23,7 @@ import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message +import org.scottishtecharmy.soundscape.resources.sleep_wake_on_leave import org.scottishtecharmy.soundscape.resources.sleep_wake_up_now import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors import org.scottishtecharmy.soundscape.ui.theme.largePadding @@ -61,22 +62,43 @@ fun SharedSleepScreen( modifier = Modifier.largePadding(), ) } - Row { - Button( + Row( + modifier = Modifier.fillMaxWidth() + ) { + WakeButton( + text = stringResource(Res.string.sleep_wake_up_now), onClick = onExit, - modifier = Modifier - .fillMaxWidth() - .height(spacing.targetSize * 4) - .testTag("sleepWakeUpNow"), - shape = RoundedCornerShape(spacing.tiny), - colors = if (!LocalInspectionMode.current) currentAppButtonColors else ButtonDefaults.buttonColors(), - ) { - Text( - text = stringResource(Res.string.sleep_wake_up_now), - textAlign = TextAlign.Start, - style = MaterialTheme.typography.displaySmall, - ) - } + modifier = Modifier.fillMaxWidth(0.5f), + ) + WakeButton( + text = stringResource(Res.string.sleep_wake_on_leave), + onClick = { + // TODO + }, + modifier = Modifier, + ) } } } + +@Composable +fun WakeButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = onClick, + modifier = modifier + .height(spacing.targetSize * 4) + .testTag("sleepWakeUpNow"), + shape = RoundedCornerShape(spacing.tiny), + colors = if (!LocalInspectionMode.current) currentAppButtonColors else ButtonDefaults.buttonColors(), + ) { + Text( + text = text, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.displaySmall, + ) + } +} From 1a0638241a833fc24ddd85a50a6fb9e97023aa8e Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 16:20:14 +0100 Subject: [PATCH 03/17] - Implementing state of Shared Sleep screen. --- .../home/home/SharedSleepScreen.android.kt | 20 +++++++ .../home/home/SharedSleepScreenPreview.kt | 35 +++++++++++- .../composeResources/values/strings.xml | 2 + .../soundscape/navigation/SharedNavGraph.kt | 6 +++ .../screens/home/home/SharedSleepScreen.kt | 54 +++++++++++++++---- .../home/home/SharedSleepScreen.ios.kt | 19 +++++++ 6 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt create mode 100644 shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt new file mode 100644 index 000000000..b60d0f822 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt @@ -0,0 +1,20 @@ +package org.scottishtecharmy.soundscape.screens.home.home + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { + return SleepScreenViewModel() +} + +class SleepScreenViewModel : ViewModel(), ISleepScreenViewModel { + private val _state: MutableStateFlow = MutableStateFlow(SleepScreenState()) + override val state: StateFlow = _state.asStateFlow() + + override fun onWakeOnLeaveClicked() { + _state.update { it.copy(wakeOnLeaveEnabled = !_state.value.wakeOnLeaveEnabled) } + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt index 8d4206451..5dc0bfcf9 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -8,7 +8,20 @@ import androidx.compose.ui.tooling.preview.Preview fun SharedSleepScreenPreview() { SharedSleepScreen( onWakeUp = {}, - onExit = {} + onExit = {}, + onWakeOnLeaveClicked = { }, + wakeOnLeaveEnabled = false, + ) +} + +@Preview(showBackground = true) +@Composable +fun SharedSleepScreenWakeOnLeaveEnabledPreview() { + SharedSleepScreen( + onWakeUp = {}, + onExit = {}, + onWakeOnLeaveClicked = { }, + wakeOnLeaveEnabled = true, ) } @@ -19,4 +32,24 @@ fun WakeButtonPreview() { text = "Wake On Leave", onClick = { }, ) +} + +@Preview(showBackground = true) +@Composable +fun WakeButtonsWakeOnLeaveVisiblePreview() { + WakeButtons( + wakeUpNowOnClick = {}, + wakeOnLeaveOnClick = {}, + showWakeOnLeave = true, + ) +} + +@Preview(showBackground = true) +@Composable +fun WakeButtonsWakeOnLeaveNotVisiblePreview() { + WakeButtons( + wakeUpNowOnClick = {}, + wakeOnLeaveOnClick = {}, + showWakeOnLeave = false, + ) } \ No newline at end of file diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index f45196d45..5ae993b9a 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -338,6 +338,8 @@ Sleeping Soundscape is currently sleeping. This prevents Soundscape from using Location Services or downloading data. This conserves battery when you are not using Soundscape. Tap the button below to wake up Soundscape. + + Soundscape is currently snoozing. When you leave your current location, Soundscape will automatically wake up. This uses Location Services in a low power mode to conserve your phone's battery. Tap the button below to wake up now. put Soundscape to sleep diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index 1f1ae8eea..f10d65a26 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -45,6 +45,7 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHelpScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen +import org.scottishtecharmy.soundscape.screens.home.home.provideSleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen import org.scottishtecharmy.soundscape.screens.home.offlinemaps.NearbyExtractsState @@ -570,11 +571,16 @@ fun SharedNavHost( } composable(SharedRoutes.SLEEP) { + val viewModel = provideSleepScreenViewModel() + val state = viewModel.state.collectAsState() + SharedSleepScreen( onWakeUp = callbacks.onWakeUp, onExit = { navController.popBackStack(SharedRoutes.HOME, inclusive = false) }, + onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, + wakeOnLeaveEnabled = state.value.wakeOnLeaveEnabled, ) } diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 52ade6644..96d20b52b 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -19,20 +19,35 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign +import kotlinx.coroutines.flow.StateFlow import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message +import org.scottishtecharmy.soundscape.resources.sleep_sleeping_wake_on_leave_message import org.scottishtecharmy.soundscape.resources.sleep_wake_on_leave import org.scottishtecharmy.soundscape.resources.sleep_wake_up_now import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors import org.scottishtecharmy.soundscape.ui.theme.largePadding import org.scottishtecharmy.soundscape.ui.theme.spacing +data class SleepScreenState( + val wakeOnLeaveEnabled: Boolean = false, +) + +interface ISleepScreenViewModel { + val state: StateFlow + fun onWakeOnLeaveClicked() +} + +expect fun provideSleepScreenViewModel(): ISleepScreenViewModel + @Composable fun SharedSleepScreen( onWakeUp: () -> Unit, onExit: () -> Unit, + onWakeOnLeaveClicked: () -> Unit, + wakeOnLeaveEnabled: Boolean, modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { @@ -56,25 +71,42 @@ fun SharedSleepScreen( } Row { Text( - text = stringResource(Res.string.sleep_sleeping_message), + text = stringResource( + if (wakeOnLeaveEnabled) + Res.string.sleep_sleeping_wake_on_leave_message else + Res.string.sleep_sleeping_message + ), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.largePadding(), ) } - Row( + WakeButtons( + wakeUpNowOnClick = onExit, + wakeOnLeaveOnClick = onWakeOnLeaveClicked, + showWakeOnLeave = wakeOnLeaveEnabled, modifier = Modifier.fillMaxWidth() - ) { - WakeButton( - text = stringResource(Res.string.sleep_wake_up_now), - onClick = onExit, - modifier = Modifier.fillMaxWidth(0.5f), - ) + ) + } +} + +@Composable +fun WakeButtons( + wakeUpNowOnClick: () -> Unit, + wakeOnLeaveOnClick: () -> Unit, + showWakeOnLeave: Boolean, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + WakeButton( + text = stringResource(Res.string.sleep_wake_up_now), + onClick = wakeUpNowOnClick, + modifier = Modifier.fillMaxWidth(if (showWakeOnLeave) 0.5f else 1.0f), + ) + if (showWakeOnLeave) { WakeButton( text = stringResource(Res.string.sleep_wake_on_leave), - onClick = { - // TODO - }, + onClick = wakeOnLeaveOnClick, modifier = Modifier, ) } diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt new file mode 100644 index 000000000..d674982d2 --- /dev/null +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt @@ -0,0 +1,19 @@ +package org.scottishtecharmy.soundscape.screens.home.home + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { + return SleepScreenViewModel() +} + +class SleepScreenViewModel : ISleepScreenViewModel { + private val _state: MutableStateFlow = MutableStateFlow(SleepScreenState()) + override val state: StateFlow = _state.asStateFlow() + + override fun onWakeOnLeaveClicked() { + _state.update { it.copy(wakeOnLeaveEnabled = !_state.value.wakeOnLeaveEnabled) } + } +} \ No newline at end of file From b26fda34edfa781e2a82bf1237b83405efafc007 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 16:34:49 +0100 Subject: [PATCH 04/17] - Refactoring to use state to display. - Hoisted state out into parent composable. --- .../home/home/SharedSleepScreen.android.kt | 16 ++++++++-- .../home/home/SharedSleepScreenPreview.kt | 8 ++--- .../soundscape/navigation/SharedNavGraph.kt | 2 +- .../screens/home/home/SharedSleepScreen.kt | 30 ++++++++++++------- .../home/home/SharedSleepScreen.ios.kt | 16 ++++++++-- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt index b60d0f822..bd7bef92a 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt @@ -5,16 +5,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { return SleepScreenViewModel() } class SleepScreenViewModel : ViewModel(), ISleepScreenViewModel { - private val _state: MutableStateFlow = MutableStateFlow(SleepScreenState()) + private val _state: MutableStateFlow = + MutableStateFlow(Sleeping) override val state: StateFlow = _state.asStateFlow() override fun onWakeOnLeaveClicked() { - _state.update { it.copy(wakeOnLeaveEnabled = !_state.value.wakeOnLeaveEnabled) } + _state.update { + when (it) { + is Sleeping -> Snoozing( + userLocation = LngLatAlt() + ) + + is Snoozing -> Sleeping + } + } } } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt index 5dc0bfcf9..8f9dea35c 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -10,7 +10,7 @@ fun SharedSleepScreenPreview() { onWakeUp = {}, onExit = {}, onWakeOnLeaveClicked = { }, - wakeOnLeaveEnabled = false, + state = SleepScreenState.Sleeping, ) } @@ -21,7 +21,7 @@ fun SharedSleepScreenWakeOnLeaveEnabledPreview() { onWakeUp = {}, onExit = {}, onWakeOnLeaveClicked = { }, - wakeOnLeaveEnabled = true, + state = SleepScreenState.Snoozing() ) } @@ -40,7 +40,7 @@ fun WakeButtonsWakeOnLeaveVisiblePreview() { WakeButtons( wakeUpNowOnClick = {}, wakeOnLeaveOnClick = {}, - showWakeOnLeave = true, + sleepScreenState = SleepScreenState.Sleeping, ) } @@ -50,6 +50,6 @@ fun WakeButtonsWakeOnLeaveNotVisiblePreview() { WakeButtons( wakeUpNowOnClick = {}, wakeOnLeaveOnClick = {}, - showWakeOnLeave = false, + sleepScreenState = SleepScreenState.Snoozing(), ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index f10d65a26..bd8931d5a 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -580,7 +580,7 @@ fun SharedNavHost( navController.popBackStack(SharedRoutes.HOME, inclusive = false) }, onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, - wakeOnLeaveEnabled = state.value.wakeOnLeaveEnabled, + state = state.value, ) } diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 96d20b52b..1706a7805 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import kotlinx.coroutines.flow.StateFlow import org.jetbrains.compose.resources.stringResource +import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message @@ -31,9 +32,10 @@ import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors import org.scottishtecharmy.soundscape.ui.theme.largePadding import org.scottishtecharmy.soundscape.ui.theme.spacing -data class SleepScreenState( - val wakeOnLeaveEnabled: Boolean = false, -) +sealed class SleepScreenState { + object Sleeping : SleepScreenState() + data class Snoozing(val userLocation: LngLatAlt? = LngLatAlt()) : SleepScreenState() +} interface ISleepScreenViewModel { val state: StateFlow @@ -47,7 +49,7 @@ fun SharedSleepScreen( onWakeUp: () -> Unit, onExit: () -> Unit, onWakeOnLeaveClicked: () -> Unit, - wakeOnLeaveEnabled: Boolean, + state: SleepScreenState, modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { @@ -72,9 +74,10 @@ fun SharedSleepScreen( Row { Text( text = stringResource( - if (wakeOnLeaveEnabled) - Res.string.sleep_sleeping_wake_on_leave_message else - Res.string.sleep_sleeping_message + when (state) { + SleepScreenState.Sleeping -> Res.string.sleep_sleeping_message + is SleepScreenState.Snoozing -> Res.string.sleep_sleeping_wake_on_leave_message + } ), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, @@ -84,7 +87,7 @@ fun SharedSleepScreen( WakeButtons( wakeUpNowOnClick = onExit, wakeOnLeaveOnClick = onWakeOnLeaveClicked, - showWakeOnLeave = wakeOnLeaveEnabled, + sleepScreenState = state, modifier = Modifier.fillMaxWidth() ) } @@ -94,16 +97,21 @@ fun SharedSleepScreen( fun WakeButtons( wakeUpNowOnClick: () -> Unit, wakeOnLeaveOnClick: () -> Unit, - showWakeOnLeave: Boolean, + sleepScreenState: SleepScreenState, modifier: Modifier = Modifier, ) { Row(modifier = modifier) { WakeButton( text = stringResource(Res.string.sleep_wake_up_now), onClick = wakeUpNowOnClick, - modifier = Modifier.fillMaxWidth(if (showWakeOnLeave) 0.5f else 1.0f), + modifier = Modifier.fillMaxWidth( + when (sleepScreenState) { + SleepScreenState.Sleeping -> 0.5f + is SleepScreenState.Snoozing -> 1.0f + } + ) ) - if (showWakeOnLeave) { + if (sleepScreenState is SleepScreenState.Sleeping) { WakeButton( text = stringResource(Res.string.sleep_wake_on_leave), onClick = wakeOnLeaveOnClick, diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt index d674982d2..ce818a456 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt @@ -4,16 +4,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { return SleepScreenViewModel() } class SleepScreenViewModel : ISleepScreenViewModel { - private val _state: MutableStateFlow = MutableStateFlow(SleepScreenState()) + private val _state: MutableStateFlow = + MutableStateFlow(Sleeping) override val state: StateFlow = _state.asStateFlow() override fun onWakeOnLeaveClicked() { - _state.update { it.copy(wakeOnLeaveEnabled = !_state.value.wakeOnLeaveEnabled) } + _state.update { + when (it) { + is Sleeping -> Snoozing( + userLocation = LngLatAlt() + ) + + is Snoozing -> Sleeping + } + } } } \ No newline at end of file From 65ff0b0a842c586f03af7ac13065753e16a6b4a5 Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 17:12:55 +0100 Subject: [PATCH 05/17] - Moved AndroidLocationProvider to shared module to fulfill "expect" function. - Implementing functionality for GeoFence. --- .../AndroidLocationProvider.kt | 0 .../locationprovider/LocationConversions.kt | 0 .../home/home/SharedSleepScreen.android.kt | 79 +++++++++++++++++-- .../soundscape/navigation/SharedNavGraph.kt | 8 +- .../screens/home/home/SharedSleepScreen.kt | 10 ++- .../home/home/SharedSleepScreen.ios.kt | 14 +++- 6 files changed, 100 insertions(+), 11 deletions(-) rename {app/src/main/java => shared/src/androidMain/kotlin}/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt (100%) rename {app/src/main/java => shared/src/androidMain/kotlin}/org/scottishtecharmy/soundscape/locationprovider/LocationConversions.kt (100%) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt similarity index 100% rename from app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt rename to shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/LocationConversions.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationConversions.kt similarity index 100% rename from app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/LocationConversions.kt rename to shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationConversions.kt diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt index bd7bef92a..a6f954923 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt @@ -1,32 +1,95 @@ package org.scottishtecharmy.soundscape.screens.home.home +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.ViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.locationprovider.AndroidLocationProvider +import org.scottishtecharmy.soundscape.locationprovider.LocationProvider +import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing -actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { - return SleepScreenViewModel() +actual fun provideSleepScreenViewModel( + locationProvider: LocationProvider, + coroutineScope: CoroutineScope, +): ISleepScreenViewModel { + return SleepScreenViewModel(locationProvider, coroutineScope) } -class SleepScreenViewModel : ViewModel(), ISleepScreenViewModel { +fun SoundscapeLocation.asLngLatAlt(): LngLatAlt { + return LngLatAlt( + longitude = longitude, + latitude = latitude, + ) +} + +class SleepScreenViewModel( + private val locationProvider: LocationProvider, + private val coroutineScope: CoroutineScope, +) : ViewModel(), + ISleepScreenViewModel { private val _state: MutableStateFlow = MutableStateFlow(Sleeping) override val state: StateFlow = _state.asStateFlow() + private val _location: MutableStateFlow = MutableStateFlow(LngLatAlt()) + + private lateinit var _locationJob: Job + override fun onWakeOnLeaveClicked() { _state.update { when (it) { - is Sleeping -> Snoozing( - userLocation = LngLatAlt() - ) + is Sleeping -> { + subscribeToLocation() + Snoozing(_location.value) + } + + is Snoozing -> { + unsubscribeFromLocation() + Sleeping + } + } + } + } - is Snoozing -> Sleeping + private fun unsubscribeFromLocation() { + if (this::_locationJob.isInitialized) { + _locationJob.cancel() + } + _state.update { Sleeping } + } + + private fun subscribeToLocation() { + _locationJob = coroutineScope.launch { + locationProvider.locationFlow.collect { loc -> + if (isActive) { + val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() + _location.update { lngLat } + _state.update { + if (it is Snoozing) it.copy(userLocation = lngLat) else it + } + } } } } -} \ No newline at end of file +} + +@Composable +actual fun provideLocationProvider(): LocationProvider { + val context = LocalContext.current.applicationContext + return remember { + AndroidLocationProvider(context).apply { + start(context) + } + } +} diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index bd8931d5a..d96361121 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -45,6 +46,7 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHelpScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen +import org.scottishtecharmy.soundscape.screens.home.home.provideLocationProvider import org.scottishtecharmy.soundscape.screens.home.home.provideSleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen @@ -571,7 +573,11 @@ fun SharedNavHost( } composable(SharedRoutes.SLEEP) { - val viewModel = provideSleepScreenViewModel() + val locationProvider = provideLocationProvider() + val scope = rememberCoroutineScope() + val viewModel = remember(locationProvider, scope) { + provideSleepScreenViewModel(locationProvider, scope) + } val state = viewModel.state.collectAsState() SharedSleepScreen( diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 1706a7805..c16b2e3b9 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -19,9 +19,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message @@ -42,7 +44,13 @@ interface ISleepScreenViewModel { fun onWakeOnLeaveClicked() } -expect fun provideSleepScreenViewModel(): ISleepScreenViewModel +@Composable +expect fun provideLocationProvider(): LocationProvider + +expect fun provideSleepScreenViewModel( + locationProvider: LocationProvider, + coroutineScope: CoroutineScope, +): ISleepScreenViewModel @Composable fun SharedSleepScreen( diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt index ce818a456..87f5c2e59 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt @@ -1,17 +1,29 @@ package org.scottishtecharmy.soundscape.screens.home.home +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing -actual fun provideSleepScreenViewModel(): ISleepScreenViewModel { +actual fun provideSleepScreenViewModel( + locationProvider: LocationProvider, + coroutineScope: CoroutineScope, +): ISleepScreenViewModel { return SleepScreenViewModel() } +@Composable +actual fun provideLocationProvider(): LocationProvider { + return remember { org.scottishtecharmy.soundscape.locationprovider.IosLocationProvider() } +} + class SleepScreenViewModel : ISleepScreenViewModel { private val _state: MutableStateFlow = MutableStateFlow(Sleeping) From 920cbdbcd24937733d4ce4c31d7a46d3a69e1efc Mon Sep 17 00:00:00 2001 From: luke Date: Fri, 17 Jul 2026 17:53:30 +0100 Subject: [PATCH 06/17] - Added "start" function to abstract LocationProvider class to manually invoke start in SharedSleepScreen - Removed "context" from "start" as it was no longer required - Moved AndroidLocationProvider back to app module and used AppCallback for function to provide LocationProvider in shared module --- .../locationprovider/AndroidLocationProvider.kt | 2 +- .../GooglePlayLocationProvider.kt | 2 +- .../soundscape/screens/home/HomeScreen.kt | 11 ++++++++++- .../soundscape/services/SoundscapeService.kt | 4 ++-- .../home/home/SharedSleepScreen.android.kt | 16 ++-------------- .../composeResources/values/strings.xml | 2 ++ .../org/scottishtecharmy/soundscape/App.kt | 2 ++ .../locationprovider/LocationProvider.kt | 2 ++ .../locationprovider/StaticLocationProvider.kt | 2 +- .../soundscape/navigation/SharedNavGraph.kt | 8 ++++++-- .../screens/home/home/SharedSleepScreen.kt | 11 +++++++---- .../soundscape/MainViewController.kt | 1 + .../locationprovider/IosLocationProvider.kt | 2 +- .../screens/home/home/SharedSleepScreen.ios.kt | 7 ------- 14 files changed, 38 insertions(+), 34 deletions(-) rename {shared/src/androidMain/kotlin => app/src/main/java}/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt (99%) diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt similarity index 99% rename from shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt rename to app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt index 32fea2e6a..0ec394496 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt @@ -88,7 +88,7 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { } @SuppressLint("MissingPermission") - fun start(context: Context) { + override fun start() { if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt index 5b11aea3a..cf91c5bf6 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt @@ -72,7 +72,7 @@ class GooglePlayLocationProvider(context: Context) : } @SuppressLint("MissingPermission") - fun start(context: Context) { + override fun start() { fusedLocationClient.requestLocationUpdates( LocationRequest.Builder( diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/HomeScreen.kt b/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/HomeScreen.kt index 68942c4a8..1def11ac6 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/HomeScreen.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/screens/home/HomeScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -222,6 +221,16 @@ fun HomeScreen( activity.setServiceState(false) }, onGetLanguageMismatch = { getLanguageMismatch() }, + provideLocationProvider = { + if (org.scottishtecharmy.soundscape.hasPlayServices(context)) { + org.scottishtecharmy.soundscape.locationprovider.GooglePlayLocationProvider( + context + ).apply { start() } + } else { + org.scottishtecharmy.soundscape.locationprovider.AndroidLocationProvider(context) + .apply { start() } + } + }, getOpenSourceLicensesJson = { context.assets.open("open_source_licenses.json") .bufferedReader() diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt index 91437ac29..66bc25231 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/services/SoundscapeService.kt @@ -407,8 +407,8 @@ class SoundscapeService : MediaSessionService(), GeoEngineListener, MediaControl private fun startProviders() { when (val lp = locationProvider) { - is GooglePlayLocationProvider -> lp.start(this) - is AndroidLocationProvider -> lp.start(this) + is GooglePlayLocationProvider -> lp.start() + is AndroidLocationProvider -> lp.start() is StaticLocationProvider -> lp.start() } when (val dp = directionProvider) { diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt index a6f954923..c6e389192 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt @@ -1,8 +1,5 @@ package org.scottishtecharmy.soundscape.screens.home.home -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.ViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -13,7 +10,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.locationprovider.AndroidLocationProvider import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping @@ -66,11 +62,13 @@ class SleepScreenViewModel( if (this::_locationJob.isInitialized) { _locationJob.cancel() } + locationProvider.destroy() _state.update { Sleeping } } private fun subscribeToLocation() { _locationJob = coroutineScope.launch { + locationProvider.start() locationProvider.locationFlow.collect { loc -> if (isActive) { val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() @@ -83,13 +81,3 @@ class SleepScreenViewModel( } } } - -@Composable -actual fun provideLocationProvider(): LocationProvider { - val context = LocalContext.current.applicationContext - return remember { - AndroidLocationProvider(context).apply { - start(context) - } - } -} diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 5ae993b9a..4b1239f1d 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -336,6 +336,8 @@ Sleep Sleeping + + Snoozing Soundscape is currently sleeping. This prevents Soundscape from using Location Services or downloading data. This conserves battery when you are not using Soundscape. Tap the button below to wake up Soundscape. diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/App.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/App.kt index 59fa8685f..03a326d72 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/App.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/App.kt @@ -16,6 +16,7 @@ import org.scottishtecharmy.soundscape.geojsonparser.geojson.Feature import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.intents.IncomingIntent import org.scottishtecharmy.soundscape.locationprovider.DeviceDirection +import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import org.scottishtecharmy.soundscape.navigation.NavigationStateHolder import org.scottishtecharmy.soundscape.navigation.SharedNavHost @@ -98,6 +99,7 @@ data class AppCallbacks( }, val onSetApplicationLocale: (String?) -> Unit = {}, val onGetLanguageMismatch: () -> Language? = { null }, + val provideLocationProvider: (() -> LocationProvider)? = null, val getOpenSourceLicensesJson: (() -> String)? = null, /** * Wipes preferences and immediately restarts the app. When non-null, diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt index c5d8bdc7b..ac713887f 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt @@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow abstract class LocationProvider { + + abstract fun start() abstract fun destroy() open fun updateLocation(newLocation: SoundscapeLocation) {} diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt index 1845f10ca..93b6b42e9 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt @@ -6,7 +6,7 @@ class StaticLocationProvider(private var location: LngLatAlt) : LocationProvider override fun destroy() {} - fun start() { + override fun start() { val loc = SoundscapeLocation( latitude = location.latitude, longitude = location.longitude, diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index d96361121..76943bbdf 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -46,7 +46,6 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHelpScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen -import org.scottishtecharmy.soundscape.screens.home.home.provideLocationProvider import org.scottishtecharmy.soundscape.screens.home.home.provideSleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen @@ -573,7 +572,12 @@ fun SharedNavHost( } composable(SharedRoutes.SLEEP) { - val locationProvider = provideLocationProvider() + val locationProvider = remember { + callbacks.provideLocationProvider?.invoke() + ?: org.scottishtecharmy.soundscape.locationprovider.StaticLocationProvider( + LngLatAlt() + ) + } val scope = rememberCoroutineScope() val viewModel = remember(locationProvider, scope) { provideSleepScreenViewModel(locationProvider, scope) diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index c16b2e3b9..a4739dbc3 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -28,6 +28,7 @@ import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message import org.scottishtecharmy.soundscape.resources.sleep_sleeping_wake_on_leave_message +import org.scottishtecharmy.soundscape.resources.sleep_snoozing import org.scottishtecharmy.soundscape.resources.sleep_wake_on_leave import org.scottishtecharmy.soundscape.resources.sleep_wake_up_now import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors @@ -44,9 +45,6 @@ interface ISleepScreenViewModel { fun onWakeOnLeaveClicked() } -@Composable -expect fun provideLocationProvider(): LocationProvider - expect fun provideSleepScreenViewModel( locationProvider: LocationProvider, coroutineScope: CoroutineScope, @@ -73,7 +71,12 @@ fun SharedSleepScreen( ) { Row { Text( - text = stringResource(Res.string.sleep_sleeping), + text = stringResource( + when (state) { + SleepScreenState.Sleeping -> Res.string.sleep_sleeping + is SleepScreenState.Snoozing -> Res.string.sleep_snoozing + } + ), style = MaterialTheme.typography.titleLarge, modifier = Modifier.largePadding(), color = MaterialTheme.colorScheme.onSurface, diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/MainViewController.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/MainViewController.kt index ba1132824..056d5b9b1 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/MainViewController.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/MainViewController.kt @@ -302,6 +302,7 @@ fun MainViewController() = ComposeUIViewController { onGetLanguageMismatch = { org.scottishtecharmy.soundscape.screens.onboarding.language.getLanguageMismatch() }, + provideLocationProvider = { service.iosLocationProvider }, getOpenSourceLicensesJson = { readResourceText("open_source_licenses.json") }, // No onResetSettings hook — SharedNavGraph clears the // PreferencesProvider and navigates to the onboarding flow when diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt index 4fdd26ad6..5db7d0359 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt @@ -27,7 +27,7 @@ class IosLocationProvider : LocationProvider() { locationManager.requestAlwaysAuthorization() } - fun start() { + override fun start() { locationManager.delegate = delegate locationManager.startUpdatingLocation() } diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt index 87f5c2e59..e364047b7 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt @@ -1,7 +1,5 @@ package org.scottishtecharmy.soundscape.screens.home.home -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -19,11 +17,6 @@ actual fun provideSleepScreenViewModel( return SleepScreenViewModel() } -@Composable -actual fun provideLocationProvider(): LocationProvider { - return remember { org.scottishtecharmy.soundscape.locationprovider.IosLocationProvider() } -} - class SleepScreenViewModel : ISleepScreenViewModel { private val _state: MutableStateFlow = MutableStateFlow(Sleeping) From 21b15d5ea29d49b9b8cb19e2e68df5f2cc3f8fd5 Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 20 Jul 2026 09:02:51 +0100 Subject: [PATCH 07/17] - Removed redundant platform specific code as dependencies are provided. - Managing exit of screen - Created events shared flow to manage button clicks and trigger functionality required. --- .../home/home/SharedSleepScreen.android.kt | 83 ------------ .../home/home/SharedSleepScreenPreview.kt | 4 +- .../soundscape/navigation/SharedNavGraph.kt | 26 ++-- .../screens/home/home/SharedSleepScreen.kt | 121 ++++++++++++++++-- .../home/home/SharedSleepScreen.ios.kt | 36 ------ 5 files changed, 130 insertions(+), 140 deletions(-) delete mode 100644 shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt delete mode 100644 shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt deleted file mode 100644 index c6e389192..000000000 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.android.kt +++ /dev/null @@ -1,83 +0,0 @@ -package org.scottishtecharmy.soundscape.screens.home.home - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.locationprovider.LocationProvider -import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing - -actual fun provideSleepScreenViewModel( - locationProvider: LocationProvider, - coroutineScope: CoroutineScope, -): ISleepScreenViewModel { - return SleepScreenViewModel(locationProvider, coroutineScope) -} - -fun SoundscapeLocation.asLngLatAlt(): LngLatAlt { - return LngLatAlt( - longitude = longitude, - latitude = latitude, - ) -} - -class SleepScreenViewModel( - private val locationProvider: LocationProvider, - private val coroutineScope: CoroutineScope, -) : ViewModel(), - ISleepScreenViewModel { - private val _state: MutableStateFlow = - MutableStateFlow(Sleeping) - override val state: StateFlow = _state.asStateFlow() - - private val _location: MutableStateFlow = MutableStateFlow(LngLatAlt()) - - private lateinit var _locationJob: Job - - override fun onWakeOnLeaveClicked() { - _state.update { - when (it) { - is Sleeping -> { - subscribeToLocation() - Snoozing(_location.value) - } - - is Snoozing -> { - unsubscribeFromLocation() - Sleeping - } - } - } - } - - private fun unsubscribeFromLocation() { - if (this::_locationJob.isInitialized) { - _locationJob.cancel() - } - locationProvider.destroy() - _state.update { Sleeping } - } - - private fun subscribeToLocation() { - _locationJob = coroutineScope.launch { - locationProvider.start() - locationProvider.locationFlow.collect { loc -> - if (isActive) { - val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() - _location.update { lngLat } - _state.update { - if (it is Snoozing) it.copy(userLocation = lngLat) else it - } - } - } - } - } -} diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt index 8f9dea35c..26cb7d1f7 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview fun SharedSleepScreenPreview() { SharedSleepScreen( onWakeUp = {}, - onExit = {}, + onWakeUpNowClicked = {}, onWakeOnLeaveClicked = { }, state = SleepScreenState.Sleeping, ) @@ -19,7 +19,7 @@ fun SharedSleepScreenPreview() { fun SharedSleepScreenWakeOnLeaveEnabledPreview() { SharedSleepScreen( onWakeUp = {}, - onExit = {}, + onWakeUpNowClicked = {}, onWakeOnLeaveClicked = { }, state = SleepScreenState.Snoozing() ) diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index 76943bbdf..621da972f 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -46,7 +46,8 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHelpScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen -import org.scottishtecharmy.soundscape.screens.home.home.provideSleepScreenViewModel +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen import org.scottishtecharmy.soundscape.screens.home.offlinemaps.NearbyExtractsState @@ -580,18 +581,25 @@ fun SharedNavHost( } val scope = rememberCoroutineScope() val viewModel = remember(locationProvider, scope) { - provideSleepScreenViewModel(locationProvider, scope) + SleepScreenViewModel(locationProvider, scope) } + val state = viewModel.state.collectAsState() - SharedSleepScreen( - onWakeUp = callbacks.onWakeUp, - onExit = { + when (state.value) { + SleepScreenState.Exiting -> { navController.popBackStack(SharedRoutes.HOME, inclusive = false) - }, - onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, - state = state.value, - ) + } + + else -> { + SharedSleepScreen( + onWakeUp = callbacks.onWakeUp, + onWakeUpNowClicked = { viewModel.onWakeUpNowClicked() }, + onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, + state = state.value, + ) + } + } } composable(SharedRoutes.SETTINGS) { diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index a4739dbc3..e0ca006b6 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -19,11 +19,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.ViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.locationprovider.LocationProvider +import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message @@ -35,25 +44,114 @@ import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors import org.scottishtecharmy.soundscape.ui.theme.largePadding import org.scottishtecharmy.soundscape.ui.theme.spacing +fun SoundscapeLocation.asLngLatAlt(): LngLatAlt { + return LngLatAlt( + longitude = longitude, + latitude = latitude, + ) +} + sealed class SleepScreenState { object Sleeping : SleepScreenState() data class Snoozing(val userLocation: LngLatAlt? = LngLatAlt()) : SleepScreenState() + object Exiting : SleepScreenState() } -interface ISleepScreenViewModel { - val state: StateFlow - fun onWakeOnLeaveClicked() +sealed class SleepScreenEvent { + object WakeUpNowClick : SleepScreenEvent() + object WakeOnLeaveClick : SleepScreenEvent() } -expect fun provideSleepScreenViewModel( - locationProvider: LocationProvider, - coroutineScope: CoroutineScope, -): ISleepScreenViewModel +class SleepScreenViewModel( + private val locationProvider: LocationProvider, + private val coroutineScope: CoroutineScope, +) : ViewModel() { + private val _state: MutableStateFlow = + MutableStateFlow(SleepScreenState.Sleeping) + val state: StateFlow = _state.asStateFlow() + + private val _location: MutableStateFlow = MutableStateFlow(LngLatAlt()) + + private val _events: MutableSharedFlow = MutableSharedFlow() + + private lateinit var _locationJob: Job + private var _eventsJob: Job + + init { + _eventsJob = coroutineScope.launch { + _events.collect { + when (it) { + SleepScreenEvent.WakeUpNowClick -> { + onWakeUpNowClicked() + } + + SleepScreenEvent.WakeOnLeaveClick -> { + onWakeOnLeaveClicked() + } + } + } + } + } + + fun onWakeUpNowClicked() { + destroy() + } + + fun onWakeOnLeaveClicked() { + when (_state.value) { + is SleepScreenState.Sleeping -> { + subscribeToLocation() + _state.update { SleepScreenState.Snoozing(_location.value) } + } + + is SleepScreenState.Snoozing -> { + unsubscribeFromLocation() + _state.update { SleepScreenState.Sleeping } + } + + is SleepScreenState.Exiting -> { + destroy() + } + } + } + + private fun destroy() { + _state.update { + SleepScreenState.Exiting + } + if (_eventsJob.isActive) { + _eventsJob.cancel() + } + unsubscribeFromLocation() + } + + private fun unsubscribeFromLocation() { + if (this::_locationJob.isInitialized && _locationJob.isActive) { + _locationJob.cancel() + } + locationProvider.destroy() + } + + private fun subscribeToLocation() { + _locationJob = coroutineScope.launch { + locationProvider.start() + locationProvider.locationFlow.collect { loc -> + if (isActive) { + val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() + _location.update { lngLat } + _state.update { + if (it is SleepScreenState.Snoozing) it.copy(userLocation = lngLat) else it + } + } + } + } + } +} @Composable fun SharedSleepScreen( onWakeUp: () -> Unit, - onExit: () -> Unit, + onWakeUpNowClicked: () -> Unit, onWakeOnLeaveClicked: () -> Unit, state: SleepScreenState, modifier: Modifier = Modifier, @@ -73,8 +171,9 @@ fun SharedSleepScreen( Text( text = stringResource( when (state) { - SleepScreenState.Sleeping -> Res.string.sleep_sleeping + is SleepScreenState.Sleeping -> Res.string.sleep_sleeping is SleepScreenState.Snoozing -> Res.string.sleep_snoozing + else -> Res.string.sleep_sleeping } ), style = MaterialTheme.typography.titleLarge, @@ -88,6 +187,7 @@ fun SharedSleepScreen( when (state) { SleepScreenState.Sleeping -> Res.string.sleep_sleeping_message is SleepScreenState.Snoozing -> Res.string.sleep_sleeping_wake_on_leave_message + else -> Res.string.sleep_sleeping_message } ), style = MaterialTheme.typography.titleMedium, @@ -96,7 +196,7 @@ fun SharedSleepScreen( ) } WakeButtons( - wakeUpNowOnClick = onExit, + wakeUpNowOnClick = onWakeUpNowClicked, wakeOnLeaveOnClick = onWakeOnLeaveClicked, sleepScreenState = state, modifier = Modifier.fillMaxWidth() @@ -119,6 +219,7 @@ fun WakeButtons( when (sleepScreenState) { SleepScreenState.Sleeping -> 0.5f is SleepScreenState.Snoozing -> 1.0f + else -> 0.5f } ) ) diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt deleted file mode 100644 index e364047b7..000000000 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.ios.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.scottishtecharmy.soundscape.screens.home.home - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.locationprovider.LocationProvider -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Sleeping -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Snoozing - -actual fun provideSleepScreenViewModel( - locationProvider: LocationProvider, - coroutineScope: CoroutineScope, -): ISleepScreenViewModel { - return SleepScreenViewModel() -} - -class SleepScreenViewModel : ISleepScreenViewModel { - private val _state: MutableStateFlow = - MutableStateFlow(Sleeping) - override val state: StateFlow = _state.asStateFlow() - - override fun onWakeOnLeaveClicked() { - _state.update { - when (it) { - is Sleeping -> Snoozing( - userLocation = LngLatAlt() - ) - - is Snoozing -> Sleeping - } - } - } -} \ No newline at end of file From 8371a81531f32e3183b1aa7234f5877acc2eb63a Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 20 Jul 2026 11:59:49 +0100 Subject: [PATCH 08/17] - Added `onExit` back to composable --- .../home/home/SharedSleepScreenPreview.kt | 4 ++- .../soundscape/navigation/SharedNavGraph.kt | 27 +++++++------------ .../screens/home/home/SharedSleepScreen.kt | 6 ++++- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt index 26cb7d1f7..8f80d712d 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview fun SharedSleepScreenPreview() { SharedSleepScreen( onWakeUp = {}, + onExit = {}, onWakeUpNowClicked = {}, onWakeOnLeaveClicked = { }, state = SleepScreenState.Sleeping, @@ -19,8 +20,9 @@ fun SharedSleepScreenPreview() { fun SharedSleepScreenWakeOnLeaveEnabledPreview() { SharedSleepScreen( onWakeUp = {}, + onExit = {}, onWakeUpNowClicked = {}, - onWakeOnLeaveClicked = { }, + onWakeOnLeaveClicked = {}, state = SleepScreenState.Snoozing() ) } diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index 621da972f..c0c2f2f09 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -31,6 +31,7 @@ import org.scottishtecharmy.soundscape.audio.AudioEngine import org.scottishtecharmy.soundscape.audio.AudioTour import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.intents.IncomingIntent +import org.scottishtecharmy.soundscape.locationprovider.StaticLocationProvider import org.scottishtecharmy.soundscape.network.DownloadStateCommon import org.scottishtecharmy.soundscape.preferences.PreferenceKeys import org.scottishtecharmy.soundscape.preferences.PreferencesProvider @@ -47,6 +48,7 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Exiting import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen @@ -575,9 +577,7 @@ fun SharedNavHost( composable(SharedRoutes.SLEEP) { val locationProvider = remember { callbacks.provideLocationProvider?.invoke() - ?: org.scottishtecharmy.soundscape.locationprovider.StaticLocationProvider( - LngLatAlt() - ) + ?: StaticLocationProvider(LngLatAlt()) } val scope = rememberCoroutineScope() val viewModel = remember(locationProvider, scope) { @@ -586,20 +586,13 @@ fun SharedNavHost( val state = viewModel.state.collectAsState() - when (state.value) { - SleepScreenState.Exiting -> { - navController.popBackStack(SharedRoutes.HOME, inclusive = false) - } - - else -> { - SharedSleepScreen( - onWakeUp = callbacks.onWakeUp, - onWakeUpNowClicked = { viewModel.onWakeUpNowClicked() }, - onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, - state = state.value, - ) - } - } + SharedSleepScreen( + onWakeUp = callbacks.onWakeUp, + onExit = { navController.popBackStack(SharedRoutes.HOME, inclusive = false) }, + onWakeUpNowClicked = { viewModel.onWakeUpNowClicked() }, + onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, + state = state.value, + ) } composable(SharedRoutes.SETTINGS) { diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index e0ca006b6..232543580 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -151,6 +151,7 @@ class SleepScreenViewModel( @Composable fun SharedSleepScreen( onWakeUp: () -> Unit, + onExit: () -> Unit, onWakeUpNowClicked: () -> Unit, onWakeOnLeaveClicked: () -> Unit, state: SleepScreenState, @@ -196,7 +197,10 @@ fun SharedSleepScreen( ) } WakeButtons( - wakeUpNowOnClick = onWakeUpNowClicked, + wakeUpNowOnClick = { + onWakeUpNowClicked() + onExit() + }, wakeOnLeaveOnClick = onWakeOnLeaveClicked, sleepScreenState = state, modifier = Modifier.fillMaxWidth() From 6096eff6bbd92e80e7df51acad2a0c69bcdb6a60 Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 20 Jul 2026 12:03:08 +0100 Subject: [PATCH 09/17] - Fixed button width on Snoozing -> Sleeping state --- .../soundscape/screens/home/home/SharedSleepScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 232543580..559d778d8 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -223,7 +223,7 @@ fun WakeButtons( when (sleepScreenState) { SleepScreenState.Sleeping -> 0.5f is SleepScreenState.Snoozing -> 1.0f - else -> 0.5f + else -> 1.0f } ) ) From 2c4514bc69553eb49c7f7d0f553ed4a8e7c6be1b Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 20 Jul 2026 15:37:32 +0100 Subject: [PATCH 10/17] - The Sleep Screen VM now handles the platform callbacks directly. - Using a Channel to send effects to the composable - Removed onExit and onWakeUp as they are no longer required. --- .../home/home/SharedSleepScreenPreview.kt | 15 +- .../geojsonparser/geojson/LngLatAlt.kt | 9 ++ .../soundscape/navigation/SharedNavGraph.kt | 24 +-- .../screens/home/home/SharedSleepScreen.kt | 145 +++++++++++++----- 4 files changed, 136 insertions(+), 57 deletions(-) diff --git a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt index 8f80d712d..6f22060d4 100644 --- a/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt +++ b/shared/src/androidMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreenPreview.kt @@ -2,13 +2,12 @@ package org.scottishtecharmy.soundscape.screens.home.home import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt @Preview(showBackground = true) @Composable fun SharedSleepScreenPreview() { SharedSleepScreen( - onWakeUp = {}, - onExit = {}, onWakeUpNowClicked = {}, onWakeOnLeaveClicked = { }, state = SleepScreenState.Sleeping, @@ -19,11 +18,12 @@ fun SharedSleepScreenPreview() { @Composable fun SharedSleepScreenWakeOnLeaveEnabledPreview() { SharedSleepScreen( - onWakeUp = {}, - onExit = {}, onWakeUpNowClicked = {}, onWakeOnLeaveClicked = {}, - state = SleepScreenState.Snoozing() + state = SleepScreenState.Snoozing( + userLocation = LngLatAlt(), + startLocation = LngLatAlt() + ) ) } @@ -52,6 +52,9 @@ fun WakeButtonsWakeOnLeaveNotVisiblePreview() { WakeButtons( wakeUpNowOnClick = {}, wakeOnLeaveOnClick = {}, - sleepScreenState = SleepScreenState.Snoozing(), + sleepScreenState = SleepScreenState.Snoozing( + userLocation = LngLatAlt(), + startLocation = LngLatAlt() + ), ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geojsonparser/geojson/LngLatAlt.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geojsonparser/geojson/LngLatAlt.kt index 40a07c92c..83a343773 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geojsonparser/geojson/LngLatAlt.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/geojsonparser/geojson/LngLatAlt.kt @@ -1,5 +1,7 @@ package org.scottishtecharmy.soundscape.geojsonparser.geojson +import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation + open class LngLatAlt( var longitude: Double = 0.toDouble(), var latitude: Double = 0.toDouble(), @@ -31,3 +33,10 @@ open class LngLatAlt( return "$longitude,$latitude" } } + +fun SoundscapeLocation.asLngLatAlt(): LngLatAlt { + return LngLatAlt( + longitude = longitude, + latitude = latitude, + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt index c0c2f2f09..cb223da88 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/navigation/SharedNavGraph.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -24,6 +23,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.savedstate.read +import kotlinx.coroutines.flow.receiveAsFlow import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.AppCallbacks import org.scottishtecharmy.soundscape.AppFlows @@ -47,8 +47,7 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedHelpScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedHomeScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState -import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState.Exiting +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenEffect import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenViewModel import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen @@ -98,7 +97,7 @@ fun SharedNavHost( navStateHolder.navigateWithLocation( navController, SharedRoutes.LOCATION_DETAILS, - org.scottishtecharmy.soundscape.screens.home.data.LocationDescription( + LocationDescription( name = displayName, location = LngLatAlt(intent.longitude, intent.latitude), ), @@ -579,16 +578,23 @@ fun SharedNavHost( callbacks.provideLocationProvider?.invoke() ?: StaticLocationProvider(LngLatAlt()) } - val scope = rememberCoroutineScope() - val viewModel = remember(locationProvider, scope) { - SleepScreenViewModel(locationProvider, scope) + val viewModel = viewModel { + SleepScreenViewModel(locationProvider, callbacks.onWakeUp) } val state = viewModel.state.collectAsState() + LaunchedEffect(viewModel) { + viewModel.effects.receiveAsFlow().collect { + when (it) { + SleepScreenEffect.WakeUpNow -> { + navController.popBackStack(SharedRoutes.HOME, inclusive = false) + } + } + } + } + SharedSleepScreen( - onWakeUp = callbacks.onWakeUp, - onExit = { navController.popBackStack(SharedRoutes.HOME, inclusive = false) }, onWakeUpNowClicked = { viewModel.onWakeUpNowClicked() }, onWakeOnLeaveClicked = { viewModel.onWakeOnLeaveClicked() }, state = state.value, diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 559d778d8..261a3daa4 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -13,26 +13,29 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.ViewModel -import kotlinx.coroutines.CoroutineScope +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.scottishtecharmy.soundscape.geoengine.utils.rulers.CheapRuler import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import org.scottishtecharmy.soundscape.geojsonparser.geojson.asLngLatAlt import org.scottishtecharmy.soundscape.locationprovider.LocationProvider -import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping import org.scottishtecharmy.soundscape.resources.sleep_sleeping_message @@ -44,17 +47,13 @@ import org.scottishtecharmy.soundscape.ui.theme.currentAppButtonColors import org.scottishtecharmy.soundscape.ui.theme.largePadding import org.scottishtecharmy.soundscape.ui.theme.spacing -fun SoundscapeLocation.asLngLatAlt(): LngLatAlt { - return LngLatAlt( - longitude = longitude, - latitude = latitude, - ) -} - sealed class SleepScreenState { object Sleeping : SleepScreenState() - data class Snoozing(val userLocation: LngLatAlt? = LngLatAlt()) : SleepScreenState() - object Exiting : SleepScreenState() + data class Snoozing( + val userLocation: LngLatAlt, + val startLocation: LngLatAlt? = null, + val geofenceDistance: Double = 0.0, + ) : SleepScreenState() } sealed class SleepScreenEvent { @@ -62,23 +61,34 @@ sealed class SleepScreenEvent { object WakeOnLeaveClick : SleepScreenEvent() } +sealed class SleepScreenEffect { + object WakeUpNow : SleepScreenEffect() +} + class SleepScreenViewModel( private val locationProvider: LocationProvider, - private val coroutineScope: CoroutineScope, + private val onWakeUp: () -> Unit, ) : ViewModel() { private val _state: MutableStateFlow = MutableStateFlow(SleepScreenState.Sleeping) val state: StateFlow = _state.asStateFlow() private val _location: MutableStateFlow = MutableStateFlow(LngLatAlt()) - private val _events: MutableSharedFlow = MutableSharedFlow() + // Rendezvous Channel to handle navigation + val effects: Channel = Channel( + capacity = 1, + onBufferOverflow = BufferOverflow.SUSPEND + ) + private lateinit var _locationJob: Job - private var _eventsJob: Job + private val _eventsJob: Job + private lateinit var _geofenceJob: Job + private var _isDestroyed = false init { - _eventsJob = coroutineScope.launch { + _eventsJob = viewModelScope.launch { _events.collect { when (it) { SleepScreenEvent.WakeUpNowClick -> { @@ -101,46 +111,107 @@ class SleepScreenViewModel( when (_state.value) { is SleepScreenState.Sleeping -> { subscribeToLocation() - _state.update { SleepScreenState.Snoozing(_location.value) } + _state.update { + SleepScreenState.Snoozing( + userLocation = _location.value, + ) + } } is SleepScreenState.Snoozing -> { unsubscribeFromLocation() _state.update { SleepScreenState.Sleeping } } - - is SleepScreenState.Exiting -> { - destroy() - } } } private fun destroy() { - _state.update { - SleepScreenState.Exiting - } + if (_isDestroyed) return + _isDestroyed = true + if (_eventsJob.isActive) { _eventsJob.cancel() } unsubscribeFromLocation() + onWakeUp() + viewModelScope.launch { + effects.send(SleepScreenEffect.WakeUpNow) + } + } + + override fun onCleared() { + destroy() } private fun unsubscribeFromLocation() { if (this::_locationJob.isInitialized && _locationJob.isActive) { _locationJob.cancel() } + if (this::_geofenceJob.isInitialized && _geofenceJob.isActive) { + _geofenceJob.cancel() + } locationProvider.destroy() } private fun subscribeToLocation() { - _locationJob = coroutineScope.launch { - locationProvider.start() - locationProvider.locationFlow.collect { loc -> - if (isActive) { - val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() - _location.update { lngLat } - _state.update { - if (it is SleepScreenState.Snoozing) it.copy(userLocation = lngLat) else it + // Start location job to update internal location sharedflow + if (!this::_locationJob.isInitialized) { + _locationJob = viewModelScope.launch { + locationProvider.start() + locationProvider.locationFlow.collect { loc -> + if (isActive) { + val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() + _location.update { lngLat } + } + } + } + } + + // Start geofence job to collect from location job. On collection of value, update the state with: + // if first emission: the start location + // subsequent emissions update the distance from the start location + if (!this::_geofenceJob.isInitialized) { + _geofenceJob = viewModelScope.launch { + println("Geofencing job started: $state") + val ruler = CheapRuler(0.0) + _location.collectIndexed { index, value -> + var state = _state.value + + println("Geofencing job new location. Current State: $state") + + when (state) { + is SleepScreenState.Snoozing -> { + state = if (index == 0) { + state.copy( + userLocation = value, + startLocation = value, + geofenceDistance = ruler.distance(value, value) + ) + } else { + state.copy( + userLocation = value, + geofenceDistance = ruler.distance( + state.userLocation, + state.startLocation ?: value + ) + ) + } + + // Check if the geofence distance is greater than 12 metres and if so, end the + // session + println("Geofencing job. Distance: ${state.geofenceDistance}") + if (state.geofenceDistance > 12.0) { + destroy() + } + + _state.value = state + + println("Geofencing job new location. Updated State: ${_state.value}") + } + + SleepScreenState.Sleeping -> { + // no-op + } } } } @@ -150,17 +221,11 @@ class SleepScreenViewModel( @Composable fun SharedSleepScreen( - onWakeUp: () -> Unit, - onExit: () -> Unit, onWakeUpNowClicked: () -> Unit, onWakeOnLeaveClicked: () -> Unit, state: SleepScreenState, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - onDispose { onWakeUp() } - } - Column( modifier = modifier .fillMaxSize() @@ -174,7 +239,6 @@ fun SharedSleepScreen( when (state) { is SleepScreenState.Sleeping -> Res.string.sleep_sleeping is SleepScreenState.Snoozing -> Res.string.sleep_snoozing - else -> Res.string.sleep_sleeping } ), style = MaterialTheme.typography.titleLarge, @@ -188,7 +252,6 @@ fun SharedSleepScreen( when (state) { SleepScreenState.Sleeping -> Res.string.sleep_sleeping_message is SleepScreenState.Snoozing -> Res.string.sleep_sleeping_wake_on_leave_message - else -> Res.string.sleep_sleeping_message } ), style = MaterialTheme.typography.titleMedium, @@ -199,7 +262,6 @@ fun SharedSleepScreen( WakeButtons( wakeUpNowOnClick = { onWakeUpNowClicked() - onExit() }, wakeOnLeaveOnClick = onWakeOnLeaveClicked, sleepScreenState = state, @@ -223,7 +285,6 @@ fun WakeButtons( when (sleepScreenState) { SleepScreenState.Sleeping -> 0.5f is SleepScreenState.Snoozing -> 1.0f - else -> 1.0f } ) ) From 4e47ba4257c76780370918539054d6c6f66e2f37 Mon Sep 17 00:00:00 2001 From: luke Date: Mon, 20 Jul 2026 16:13:06 +0100 Subject: [PATCH 11/17] - Added some tests for SleepScreenViewModel --- .../home/home/SleepScreenViewModelTest.kt | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt diff --git a/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt b/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt new file mode 100644 index 000000000..601c4bdfb --- /dev/null +++ b/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt @@ -0,0 +1,90 @@ +package org.scottishtecharmy.soundscape.screens.home.home + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.scottishtecharmy.soundscape.locationprovider.LocationProvider +import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class SleepScreenViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + private val _mockLocationProvider = object : LocationProvider() { + override fun start() { + mutableLocationFlow.value = SoundscapeLocation(47.872, 50.123) + } + + override fun destroy() { + // no-op + } + } + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun onWakeOnLeaveClicked_stateUpdatesToSnoozing() = runTest { + val vm = SleepScreenViewModel(_mockLocationProvider) {} + + // Initial state should be Sleeping + assertIs(vm.state.value) + + // Trigger Snoozing + vm.onWakeOnLeaveClicked() + + // Wait for coroutines in viewModelScope (subscribeToLocation) to process + testDispatcher.scheduler.advanceUntilIdle() + + // State should now be Snoozing + assertIs(vm.state.value) + } + + @Test + fun onWakeUpNowClicked_callsOnWakeUp() = runTest { + var wakeUpCalled = false + val vm = SleepScreenViewModel(_mockLocationProvider) { + wakeUpCalled = true + } + + vm.onWakeUpNowClicked() + + assertTrue(wakeUpCalled) + } + + @Test + fun onWakeUpNowClicked_wakeUpNowEffectSent() = runTest { + val vm = SleepScreenViewModel(_mockLocationProvider) {} + + val effects = vm.effects.receiveAsFlow() + val effectsList: MutableList = mutableListOf() + val collectJob = launch { effects.toCollection(effectsList) } + + vm.onWakeUpNowClicked() + + // Wait for coroutines in viewModelScope (destroy) to process + testDispatcher.scheduler.advanceUntilIdle() + + assertIs(effectsList[0]) + collectJob.cancel() + } +} From 21f219abf208e84fa2e55e7eed1ce5d13e01b127 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 14:27:32 +0100 Subject: [PATCH 12/17] - Created "Accuracy" sealed class for LocationProvider to allow for low power location monitoring for geofencing. - Left original "start()" function in LocationProvider that now defaults to previous behaviour so to not break API. --- .../AndroidLocationProvider.kt | 48 +++++++++++-------- .../GooglePlayLocationProvider.kt | 18 +++---- .../locationprovider/LocationProvider.kt | 15 ++++++ .../StaticLocationProvider.kt | 4 ++ .../screens/home/home/SharedSleepScreen.kt | 3 +- .../locationprovider/IosLocationProvider.kt | 4 ++ 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt index 0ec394496..2442b0f89 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt @@ -12,7 +12,6 @@ import android.os.Looper import androidx.core.app.ActivityCompat import org.scottishtecharmy.soundscape.geoengine.filters.KalmanLocationFilter import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import kotlin.time.Duration.Companion.seconds class AndroidLocationProvider(context: Context) : LocationProvider() { @@ -87,31 +86,40 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { locationManager.removeUpdates(locationListener) } - @SuppressLint("MissingPermission") override fun start() { - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - LOCATION_UPDATES_INTERVAL_MS, - MIN_DISTANCE_METERS, - locationListener, - Looper.getMainLooper() - ) - } + start(Accuracy.High) + } - if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - locationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, - LOCATION_UPDATES_INTERVAL_MS, - MIN_DISTANCE_METERS, - locationListener, - Looper.getMainLooper() - ) + @SuppressLint("MissingPermission") + override fun start(accuracy: Accuracy) { + when (accuracy) { + Accuracy.High -> { + if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + accuracy.updateInterval.inWholeMilliseconds, + MIN_DISTANCE_METERS, + locationListener, + Looper.getMainLooper() + ) + } + } + + Accuracy.Balanced -> { + if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + accuracy.updateInterval.inWholeMilliseconds, + MIN_DISTANCE_METERS, + locationListener, + Looper.getMainLooper() + ) + } + } } } companion object { - private val LOCATION_UPDATES_INTERVAL_MS = 1.seconds.inWholeMilliseconds private const val MIN_DISTANCE_METERS = 1f } } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt index cf91c5bf6..a70d6320d 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt @@ -16,7 +16,6 @@ import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority import org.scottishtecharmy.soundscape.geoengine.filters.KalmanLocationFilter import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import kotlin.time.Duration.Companion.seconds class GooglePlayLocationProvider(context: Context) : LocationProvider() { @@ -71,13 +70,20 @@ class GooglePlayLocationProvider(context: Context) : fusedLocationClient.removeLocationUpdates(locationCallback) } - @SuppressLint("MissingPermission") override fun start() { + start(Accuracy.High) + } + + @SuppressLint("MissingPermission") + override fun start(accuracy: Accuracy) { fusedLocationClient.requestLocationUpdates( LocationRequest.Builder( - Priority.PRIORITY_HIGH_ACCURACY, - LOCATION_UPDATES_INTERVAL_MS + when (accuracy) { + Accuracy.Balanced -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + Accuracy.High -> Priority.PRIORITY_HIGH_ACCURACY + }, + accuracy.updateInterval.inWholeMilliseconds ).apply { setMinUpdateDistanceMeters(1f) setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL) @@ -87,8 +93,4 @@ class GooglePlayLocationProvider(context: Context) : Looper.getMainLooper(), ) } - - companion object { - private val LOCATION_UPDATES_INTERVAL_MS = 1.seconds.inWholeMilliseconds - } } diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt index ac713887f..282648eb0 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt @@ -2,10 +2,25 @@ package org.scottishtecharmy.soundscape.locationprovider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +sealed class Accuracy { + abstract val updateInterval: Duration + + object High : Accuracy() { + override val updateInterval: Duration = 1.seconds + } + + object Balanced : Accuracy() { + override val updateInterval: Duration = 30.seconds + } +} abstract class LocationProvider { abstract fun start() + abstract fun start(accuracy: Accuracy) abstract fun destroy() open fun updateLocation(newLocation: SoundscapeLocation) {} diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt index 93b6b42e9..20ae7f12e 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt @@ -17,6 +17,10 @@ class StaticLocationProvider(private var location: LngLatAlt) : LocationProvider mutableFilteredLocationFlow.value = loc } + override fun start(accuracy: Accuracy) { + start() + } + override fun updateLocation(newLocation: SoundscapeLocation) { mutableLocationFlow.value = newLocation mutableFilteredLocationFlow.value = newLocation diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt index 261a3daa4..465488ad0 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SharedSleepScreen.kt @@ -35,6 +35,7 @@ import org.jetbrains.compose.resources.stringResource import org.scottishtecharmy.soundscape.geoengine.utils.rulers.CheapRuler import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt import org.scottishtecharmy.soundscape.geojsonparser.geojson.asLngLatAlt +import org.scottishtecharmy.soundscape.locationprovider.Accuracy import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.resources.Res import org.scottishtecharmy.soundscape.resources.sleep_sleeping @@ -157,7 +158,7 @@ class SleepScreenViewModel( // Start location job to update internal location sharedflow if (!this::_locationJob.isInitialized) { _locationJob = viewModelScope.launch { - locationProvider.start() + locationProvider.start(Accuracy.Balanced) locationProvider.locationFlow.collect { loc -> if (isActive) { val lngLat = loc?.asLngLatAlt() ?: LngLatAlt() diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt index 5db7d0359..e1f16b4ef 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt @@ -28,6 +28,10 @@ class IosLocationProvider : LocationProvider() { } override fun start() { + start(Accuracy.High) + } + + override fun start(accuracy: Accuracy) { locationManager.delegate = delegate locationManager.startUpdatingLocation() } From 7e3eccab963de21e60cf8738b06df51e4f204b5c Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 14:44:56 +0100 Subject: [PATCH 13/17] - Added minimum distance property for Accuracy sealed class to allow for less frequent updates in snooze mode. --- .../soundscape/locationprovider/AndroidLocationProvider.kt | 2 +- .../soundscape/locationprovider/GooglePlayLocationProvider.kt | 2 +- .../soundscape/locationprovider/LocationProvider.kt | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt index 2442b0f89..dd407fd4f 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt @@ -110,7 +110,7 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { locationManager.requestLocationUpdates( LocationManager.NETWORK_PROVIDER, accuracy.updateInterval.inWholeMilliseconds, - MIN_DISTANCE_METERS, + accuracy.minimumDistanceM, locationListener, Looper.getMainLooper() ) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt index a70d6320d..3c07e1e88 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt @@ -85,7 +85,7 @@ class GooglePlayLocationProvider(context: Context) : }, accuracy.updateInterval.inWholeMilliseconds ).apply { - setMinUpdateDistanceMeters(1f) + setMinUpdateDistanceMeters(accuracy.minimumDistanceM) setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL) setWaitForAccurateLocation(true) }.build(), diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt index 282648eb0..69a98ba36 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt @@ -7,13 +7,17 @@ import kotlin.time.Duration.Companion.seconds sealed class Accuracy { abstract val updateInterval: Duration + abstract val minimumDistanceM: Float object High : Accuracy() { override val updateInterval: Duration = 1.seconds + override val minimumDistanceM: Float = 1.0f + } object Balanced : Accuracy() { override val updateInterval: Duration = 30.seconds + override val minimumDistanceM: Float = 5.0f } } From 38c9eb2ad355254d04eee61790d34a493fa98247 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 14:47:33 +0100 Subject: [PATCH 14/17] - Removed redundant constant --- .../soundscape/locationprovider/AndroidLocationProvider.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt index dd407fd4f..eebd2f7e6 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt @@ -98,7 +98,7 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, accuracy.updateInterval.inWholeMilliseconds, - MIN_DISTANCE_METERS, + accuracy.minimumDistanceM, locationListener, Looper.getMainLooper() ) @@ -118,8 +118,4 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { } } } - - companion object { - private const val MIN_DISTANCE_METERS = 1f - } } From fcf0c20c3c400a65af07ee17b90394a9b17df725 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 16:25:03 +0100 Subject: [PATCH 15/17] - Refactored "start()" function to use default parameter instead of having 2 functions. API will still be the same for consumers, they can just now add a parameter to request accuracy. - Using Accuracy parameter in IosLocationProvider. --- .../locationprovider/AndroidLocationProvider.kt | 4 ---- .../locationprovider/GooglePlayLocationProvider.kt | 4 ---- .../locationprovider/LocationProvider.kt | 4 +--- .../locationprovider/StaticLocationProvider.kt | 6 +----- .../locationprovider/IosLocationProvider.kt | 14 ++++++++------ 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt index eebd2f7e6..70a4eff10 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/AndroidLocationProvider.kt @@ -86,10 +86,6 @@ class AndroidLocationProvider(context: Context) : LocationProvider() { locationManager.removeUpdates(locationListener) } - override fun start() { - start(Accuracy.High) - } - @SuppressLint("MissingPermission") override fun start(accuracy: Accuracy) { when (accuracy) { diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt index 3c07e1e88..789d37548 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/locationprovider/GooglePlayLocationProvider.kt @@ -70,10 +70,6 @@ class GooglePlayLocationProvider(context: Context) : fusedLocationClient.removeLocationUpdates(locationCallback) } - override fun start() { - start(Accuracy.High) - } - @SuppressLint("MissingPermission") override fun start(accuracy: Accuracy) { diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt index 69a98ba36..e81244cc7 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/LocationProvider.kt @@ -22,9 +22,7 @@ sealed class Accuracy { } abstract class LocationProvider { - - abstract fun start() - abstract fun start(accuracy: Accuracy) + abstract fun start(accuracy: Accuracy = Accuracy.High) abstract fun destroy() open fun updateLocation(newLocation: SoundscapeLocation) {} diff --git a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt index 20ae7f12e..60c13040f 100644 --- a/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt +++ b/shared/src/commonMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/StaticLocationProvider.kt @@ -6,7 +6,7 @@ class StaticLocationProvider(private var location: LngLatAlt) : LocationProvider override fun destroy() {} - override fun start() { + override fun start(accuracy: Accuracy) { val loc = SoundscapeLocation( latitude = location.latitude, longitude = location.longitude, @@ -17,10 +17,6 @@ class StaticLocationProvider(private var location: LngLatAlt) : LocationProvider mutableFilteredLocationFlow.value = loc } - override fun start(accuracy: Accuracy) { - start() - } - override fun updateLocation(newLocation: SoundscapeLocation) { mutableLocationFlow.value = newLocation mutableFilteredLocationFlow.value = newLocation diff --git a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt index e1f16b4ef..512c2cf1f 100644 --- a/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt +++ b/shared/src/iosMain/kotlin/org/scottishtecharmy/soundscape/locationprovider/IosLocationProvider.kt @@ -7,6 +7,7 @@ import platform.CoreLocation.CLLocationManager import platform.CoreLocation.CLLocationManagerDelegateProtocol import platform.CoreLocation.kCLDistanceFilterNone import platform.CoreLocation.kCLLocationAccuracyBest +import platform.CoreLocation.kCLLocationAccuracyNearestTenMeters import platform.Foundation.NSError import platform.darwin.NSObject @@ -16,8 +17,6 @@ class IosLocationProvider : LocationProvider() { private val delegate = LocationDelegate(this) init { - locationManager.desiredAccuracy = kCLLocationAccuracyBest - locationManager.distanceFilter = kCLDistanceFilterNone locationManager.allowsBackgroundLocationUpdates = true locationManager.pausesLocationUpdatesAutomatically = false start() @@ -27,11 +26,14 @@ class IosLocationProvider : LocationProvider() { locationManager.requestAlwaysAuthorization() } - override fun start() { - start(Accuracy.High) - } - override fun start(accuracy: Accuracy) { + locationManager.desiredAccuracy = when(accuracy) { + Accuracy.High -> kCLLocationAccuracyBest + Accuracy.Balanced -> kCLLocationAccuracyNearestTenMeters + } + + locationManager.distanceFilter = accuracy.minimumDistanceM.toDouble() + locationManager.delegate = delegate locationManager.startUpdatingLocation() } From bbcd65928cfcb65128b63332c9c01f7c234c736c Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 16:35:28 +0100 Subject: [PATCH 16/17] - Fixed Preview --- .../kotlin/org/scottishtecharmy/soundscape/PreviewTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/screenshotTest/kotlin/org/scottishtecharmy/soundscape/PreviewTest.kt b/app/src/screenshotTest/kotlin/org/scottishtecharmy/soundscape/PreviewTest.kt index 33f13b86c..1585b306a 100644 --- a/app/src/screenshotTest/kotlin/org/scottishtecharmy/soundscape/PreviewTest.kt +++ b/app/src/screenshotTest/kotlin/org/scottishtecharmy/soundscape/PreviewTest.kt @@ -33,6 +33,7 @@ import org.scottishtecharmy.soundscape.screens.home.home.SharedLanguageMismatchD import org.scottishtecharmy.soundscape.screens.home.home.SharedNewReleaseDialog import org.scottishtecharmy.soundscape.screens.home.home.SharedOpenSourceLicensesScreen import org.scottishtecharmy.soundscape.screens.home.home.SharedSleepScreen +import org.scottishtecharmy.soundscape.screens.home.home.SleepScreenState import org.scottishtecharmy.soundscape.screens.home.home.StreetPreviewFunctions import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedLocationDetailsScreen import org.scottishtecharmy.soundscape.screens.home.locationDetails.SharedSaveAndEditMarkerScreen @@ -416,7 +417,12 @@ fun HomeRoutePreview() { @Preview(showBackground = true) @Composable fun SleepScreenPreview() { - SharedSleepScreen(onWakeUp = {}, onExit = {}, modifier = Modifier) + SharedSleepScreen( + modifier = Modifier, + onWakeUpNowClicked = {}, + onWakeOnLeaveClicked = {}, + state = SleepScreenState.Sleeping + ) } @Preview(showBackground = true) From 4d6bef0cdaf7c3a90cdd1be23b8ebd7d588a28b8 Mon Sep 17 00:00:00 2001 From: luke Date: Wed, 22 Jul 2026 16:44:57 +0100 Subject: [PATCH 17/17] - Fixed failing test --- .../soundscape/screens/home/home/SleepScreenViewModelTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt b/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt index 601c4bdfb..12271cf85 100644 --- a/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt +++ b/shared/src/commonTest/kotlin/org/scottishtecharmy/soundscape/screens/home/home/SleepScreenViewModelTest.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.scottishtecharmy.soundscape.locationprovider.Accuracy import org.scottishtecharmy.soundscape.locationprovider.LocationProvider import org.scottishtecharmy.soundscape.locationprovider.SoundscapeLocation import kotlin.test.AfterTest @@ -23,7 +24,7 @@ class SleepScreenViewModelTest { private val testDispatcher = StandardTestDispatcher() private val _mockLocationProvider = object : LocationProvider() { - override fun start() { + override fun start(accuracy: Accuracy) { mutableLocationFlow.value = SoundscapeLocation(47.872, 50.123) }