Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withTimeoutOrNull
import voice.core.common.DispatcherProvider
import voice.core.common.MainScope
Expand All @@ -20,10 +23,10 @@ import voice.core.data.store.SleepTimerPreferenceStore
import voice.core.logging.api.Logger
import voice.core.playback.PlayerController
import voice.core.playback.playstate.PlayStateManager
import voice.core.playback.playstate.PlayStateManager.PlayState.Playing
import kotlin.math.max
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

@SingleIn(AppScope::class)
Expand Down Expand Up @@ -95,20 +98,33 @@ class SleepTimerImpl internal constructor(
_state.value = SleepTimerState.Disabled

playerController.pauseWithRewind(fadeOutDuration)
playStateManager.flow.first { it == PlayStateManager.PlayState.Paused }

val shakeDetected = detectShakeWithTimeout()
val resetRequested = detectResetWithTimeout()
playerController.setVolume(1F)
if (shakeDetected) {
Logger.i("Shake detected, resetting timer")
if (resetRequested) {
Logger.i("Reset requested (shake or play button), resetting timer")
playerController.play()
startCountdown(duration)
}
}

private suspend fun detectShakeWithTimeout(): Boolean {
Logger.d("Waiting $SHAKE_TO_RESET_TIME for shake...")
private suspend fun detectResetWithTimeout(): Boolean {
Logger.d("Waiting $SHAKE_TO_RESET_TIME for shake or play button...")
return withTimeoutOrNull(SHAKE_TO_RESET_TIME) {
shakeDetector.detect()
coroutineScope {
val shakeJob = launch {
shakeDetector.detect()
}
val playJob = launch {
playStateManager.flow.first { it == PlayStateManager.PlayState.Playing }
}
select<Unit> {
shakeJob.onJoin { }
playJob.onJoin { }
}
coroutineContext.cancelChildren()
}
true
} ?: false
}
Expand All @@ -123,14 +139,14 @@ class SleepTimerImpl internal constructor(
}

private suspend fun suspendUntilPlaying() {
if (playStateManager.playState != Playing) {
if (playStateManager.playState != PlayStateManager.PlayState.Playing) {
Logger.i("Not playing. Waiting for playback to continue.")
playStateManager.flow.first { it == Playing }
playStateManager.flow.first { it == PlayStateManager.PlayState.Playing }
Logger.i("Playback resumed.")
}
}

internal companion object {
val SHAKE_TO_RESET_TIME = 30.seconds
val SHAKE_TO_RESET_TIME = 2.minutes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,46 @@ class SleepTimerImplTest {
sleepTimer.state.value shouldBe SleepTimerState.Disabled
}

@Test
fun `play button resets timer within window`() = testScope.runTest {
val duration = 5.seconds
sleepTimer.enable(SleepTimerMode.TimedWithDuration(duration))

// 1) Let the countdown finish and enter the shake window
advanceTimeBy(duration + 1.seconds)
runCurrent()
coVerify(exactly = 1) { playerController.pauseWithRewind(any()) }
sleepTimer.state.value shouldBe SleepTimerState.Disabled

// 2) Simulate play button press on headphones
playStateManager.playState = PlayStateManager.PlayState.Playing
runCurrent()

// 3) New countdown should start
sleepTimer.state.value shouldBe SleepTimerState.Enabled.WithDuration(duration)
// and player should be playing
verify(exactly = 1) { playerController.play() }
}

@Test
fun `play button after window does not reset timer`() = testScope.runTest {
val duration = 5.seconds
sleepTimer.enable(SleepTimerMode.TimedWithDuration(duration))

// 1) Let the countdown finish and advance past the window
advanceTimeBy(duration + 1.seconds + SleepTimerImpl.SHAKE_TO_RESET_TIME)
runCurrent()
coVerify(exactly = 1) { playerController.pauseWithRewind(any()) }
sleepTimer.state.value shouldBe SleepTimerState.Disabled

// 2) Simulate play button press on headphones
playStateManager.playState = PlayStateManager.PlayState.Playing
runCurrent()

// 3) No new countdown should start
sleepTimer.state.value shouldBe SleepTimerState.Disabled
}

companion object {

@BeforeClass
Expand Down