Skip to content

Commit

Permalink
Add sleep at end of chapter
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaThomas committed Sep 11, 2023
1 parent 5f5530f commit e6f5a5a
Show file tree
Hide file tree
Showing 18 changed files with 92 additions and 14 deletions.
7 changes: 7 additions & 0 deletions playback/src/main/kotlin/voice/playback/PlayerController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ class PlayerController
controller.setPlaybackSpeed(speed)
}

fun pauseAtStart() = executeAfterPrepare {
it.pause()
val bookId = currentBookId.data.first() ?: return@executeAfterPrepare
val book = bookRepository.get(bookId) ?: return@executeAfterPrepare
it.seekTo(book.currentMark.startMs)
}

fun setGain(gain: Decibel) = executeAfterPrepare { controller ->
controller.sendCustomCommand(CustomCommand.SetGain(gain))
}
Expand Down
13 changes: 9 additions & 4 deletions playback/src/main/kotlin/voice/playback/di/PlaybackModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dagger.Provides
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
Expand All @@ -26,6 +27,7 @@ import voice.playback.player.OnlyAudioRenderersFactory
import voice.playback.player.VoicePlayer
import voice.playback.player.onAudioSessionIdChanged
import voice.playback.playstate.PlayStateDelegatingListener
import voice.playback.playstate.PlayStateManager
import voice.playback.playstate.PositionUpdater
import voice.playback.session.LibrarySessionCallback
import voice.playback.session.PlaybackService
Expand Down Expand Up @@ -90,17 +92,20 @@ object PlaybackModule {
scope: CoroutineScope,
sleepTimer: SleepTimer,
sleepTimerCommandUpdater: SleepTimerCommandUpdater,
playStateManager: PlayStateManager,
): MediaLibraryService.MediaLibrarySession {
return MediaLibraryService.MediaLibrarySession.Builder(service, player, callback)
.setSessionActivity(mainActivityIntentProvider.toCurrentBook())
.build()
.also { session ->
scope.launch {
sleepTimer.leftSleepTimeFlow
.map { it != Duration.ZERO }
sleepTimer.leftSleepTimeFlow.map { it != Duration.ZERO }
.combine(playStateManager.sleepAtEocFlow) { sleepTimerActive, sleepEocActive ->
sleepTimerActive || sleepEocActive
}
.distinctUntilChanged()
.collect { sleepTimerActive ->
sleepTimerCommandUpdater.update(session, sleepTimerActive)
.collect { sleepActive ->
sleepTimerCommandUpdater.update(session, sleepActive)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package voice.playback.playstate

import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import voice.playback.PlayerController
import javax.inject.Inject

class PlayStateDelegatingListener
@Inject constructor(
private val playStateManager: PlayStateManager,
private val playerController: PlayerController,
) : Player.Listener {

private lateinit var player: Player
Expand All @@ -24,6 +27,13 @@ class PlayStateDelegatingListener
updatePlayState()
}

override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (playStateManager.sleepAtEoc) {
playStateManager.sleepAtEoc = false
playerController.pauseAtStart()
}
}

private fun updatePlayState() {
val playbackState = player.playbackState
playStateManager.playState = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,16 @@ constructor() {
Playing,
Paused,
}

// Sleep at eoc state
private val _sleepAtEoc = MutableStateFlow(false)

val sleepAtEocFlow: StateFlow<Boolean>
get() = _sleepAtEoc

var sleepAtEoc: Boolean
set(value) {
_sleepAtEoc.value = value
}
get() = _sleepAtEoc.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import voice.data.Book
import voice.data.repo.BookRepository
import voice.logging.core.Logger
import voice.playback.player.VoicePlayer
import voice.playback.playstate.PlayStateManager
import voice.playback.session.search.BookSearchHandler
import voice.playback.session.search.BookSearchParser
import javax.inject.Inject
Expand All @@ -44,6 +45,7 @@ class LibrarySessionCallback
private val sleepTimerCommandUpdater: SleepTimerCommandUpdater,
private val sleepTimer: SleepTimer,
private val bookRepository: BookRepository,
private val playStateManager: PlayStateManager,
) : MediaLibrarySession.Callback {

override fun onAddMediaItems(
Expand Down Expand Up @@ -198,7 +200,7 @@ class LibrarySessionCallback
): ListenableFuture<SessionResult> {
when (customCommand) {
PublishedCustomCommand.Sleep.sessionCommand -> {
sleepTimer.setActive(!sleepTimer.sleepTimerActive())
sleepTimer.setEocActive(!sleepTimer.sleepTimerActive())
}
else -> {
val command = CustomCommand.parse(customCommand, args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ interface SleepTimer {
val leftSleepTimeFlow: Flow<Duration>
fun sleepTimerActive(): Boolean
fun setActive(enable: Boolean)
fun setEocActive(enable: Boolean)
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class BookPlayController(bundle: Bundle) : ComposeController(bundle) {
onIncrementSleepTime = viewModel::incrementSleepTime,
onDecrementSleepTime = viewModel::decrementSleepTime,
onAcceptSleepTime = viewModel::onAcceptSleepTime,
onAcceptSleepAtEoc = viewModel::onAcceptSleepAtEoc,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ class BookPlayViewModel
}.collectAsState()

val sleepTime by remember { sleepTimer.leftSleepTimeFlow }.collectAsState()
val sleepAtEoc by remember { playStateManager.sleepAtEocFlow }.collectAsState()

val currentMark = book.currentChapter.markForPosition(book.content.positionInChapter)
val hasMoreThanOneChapter = book.chapters.sumOf { it.chapterMarks.count() } > 1
return BookPlayViewState(
sleepTime = sleepTime,
sleepEoc = sleepAtEoc,
playing = playState == PlayStateManager.PlayState.Playing,
title = book.content.name,
showPreviousNextButtons = hasMoreThanOneChapter,
Expand Down Expand Up @@ -145,6 +147,13 @@ class BookPlayViewModel
}
}

fun onAcceptSleepAtEoc() {
updateSleepTimeViewState {
sleepTimer.setEocActive(true)
null
}
}

private fun updateSleepTimeViewState(
update: (SleepTimerViewState) -> SleepTimerViewState?,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ data class BookPlayViewState(
val showPreviousNextButtons: Boolean,
val title: String,
val sleepTime: Duration,
val sleepEoc: Boolean,
val playedTime: Duration,
val duration: Duration,
val playing: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal fun BookPlayAppBar(
val appBarActions: @Composable RowScope.() -> Unit = {
IconButton(onClick = onSleepTimerClick) {
Icon(
imageVector = if (viewState.sleepTime == Duration.ZERO) {
imageVector = if (!viewState.sleepEoc && viewState.sleepTime == Duration.ZERO) {
Icons.Outlined.Bedtime
} else {
Icons.Outlined.BedtimeOff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal fun BookPlayContent(
cover = viewState.cover,
onPlayClick = onPlayClick,
sleepTime = viewState.sleepTime,
sleepEoc = viewState.sleepEoc,
modifier = Modifier
.fillMaxHeight()
.weight(1F)
Expand Down Expand Up @@ -75,6 +76,7 @@ internal fun BookPlayContent(
onPlayClick = onPlayClick,
cover = viewState.cover,
sleepTime = viewState.sleepTime,
sleepEoc = viewState.sleepEoc,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ private class BookPlayViewStatePreviewProvider : PreviewParameterProvider<BookPl
playing = true,
skipSilence = true,
sleepTime = 4.minutes,
sleepEoc = false,
title = "Das Ende der Welt",
)
yield(initial)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import voice.common.compose.ImmutableFile
import voice.common.formatTime
import voice.strings.R
import kotlin.time.Duration

@Composable
internal fun CoverRow(
cover: ImmutableFile?,
sleepTime: Duration,
sleepEoc: Boolean,
onPlayClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier) {
Cover(onDoubleClick = onPlayClick, cover = cover)
if (sleepTime != Duration.ZERO) {
if (sleepTime != Duration.ZERO || sleepEoc) {
Text(
modifier = Modifier
.align(Alignment.TopEnd)
Expand All @@ -33,10 +36,13 @@ internal fun CoverRow(
shape = RoundedCornerShape(20.dp),
)
.padding(horizontal = 20.dp, vertical = 16.dp),
text = formatTime(
timeMs = sleepTime.inWholeMilliseconds,
durationMs = sleepTime.inWholeMilliseconds,
),
text = when (sleepEoc) {
true -> stringResource(R.string.end_of_chapter)
false -> formatTime(
timeMs = sleepTime.inWholeMilliseconds,
durationMs = sleepTime.inWholeMilliseconds,
)
},
color = Color.White,
)
}
Expand Down
11 changes: 10 additions & 1 deletion sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class SleepTimer
}
override val leftSleepTimeFlow: StateFlow<Duration> get() = _leftSleepTime

override fun sleepTimerActive(): Boolean = sleepJob?.isActive == true && leftSleepTime > Duration.ZERO
override fun sleepTimerActive(): Boolean = (sleepJob?.isActive == true && leftSleepTime > Duration.ZERO) || playStateManager.sleepAtEoc

private var sleepJob: Job? = null

Expand All @@ -62,6 +62,14 @@ class SleepTimer
}
}

override fun setEocActive(enable: Boolean) {
if (enable) {
playStateManager.sleepAtEoc = true
} else {
cancel()
}
}

fun setActive(sleepTime: Duration = sleepTimePref.value.minutes) {
Logger.i("Starting sleepTimer. Pause in $sleepTime.")
leftSleepTime = sleepTime
Expand Down Expand Up @@ -121,5 +129,6 @@ class SleepTimer
sleepJob?.cancel()
leftSleepTime = Duration.ZERO
playerController.setVolume(1F)
playStateManager.sleepAtEoc = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fun SleepTimerDialog(
onIncrementSleepTime: () -> Unit,
onDecrementSleepTime: () -> Unit,
onAcceptSleepTime: (Int) -> Unit,
onAcceptSleepAtEoc: () -> Unit,
modifier: Modifier = Modifier,
) {
ModalBottomSheet(
Expand Down Expand Up @@ -60,6 +61,14 @@ fun SleepTimerDialog(
},
)
}
ListItem(
modifier = Modifier.clickable {
onAcceptSleepAtEoc()
},
headlineContent = {
Text(text = stringResource(id = StringsR.string.end_of_chapter))
},
)
ListItem(
modifier = Modifier.clickable {
onAcceptSleepTime(viewState.customSleepTime)
Expand Down
3 changes: 2 additions & 1 deletion strings/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<string name="notification_sleep_timer_disable">Einschlaftimer ausschalten</string>
<string name="play">Abspielen</string>
<string name="pause">Pause</string>
<string name="end_of_chapter">Kapitelende</string>
<string name="generic_error_message">Etwas ist schief gelaufen 😢</string>
<string name="generic_error_retry">Erneut versuchen</string>
<string name="cover_search_template_with_author">%1$s von %2$s Hörbuch cover</string>
Expand Down Expand Up @@ -126,4 +127,4 @@
<string name="review.request.content">Ihr Feedback ist uns wichtig! Wenn Sie einen Moment Zeit haben, bewerten Sie uns und teilen Sie Ihre Erfahrungen. Es hilft uns, Verbesserungen vorzunehmen und ein besseres Hörerlebnis zu bieten.</string>
<string name="review.feedback.title">Es tut uns leid, das zu hören!</string>
<string name="review.feedback.content">Könnten Sie uns sagen, was schief gelaufen ist\? Wir schätzen Ihr Feedback und es hilft uns, Ihre Erfahrung zu verbessern.</string>
</resources>
</resources>
3 changes: 2 additions & 1 deletion strings/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
<string name="media_session_recent">Reciente</string>
<string name="pause">Pausar</string>
<string name="play">Reproducir</string>
<string name="end_of_chapter">Fin del capítulo</string>
<string name="generic_error_retry">Reintentar</string>
<string name="generic_error_message">Algo salió mal 😢</string>
<string name="cover_search_template_no_author">%s portada del audiolibro</string>
Expand Down Expand Up @@ -128,4 +129,4 @@
<string name="review.request.content">Tu opinión nos importa. Si tienes un momento, valóranos y comparte tu experiencia. Nos ayuda a mejorar y a ofrecer una mejor experiencia auditiva.</string>
<string name="review.request.title">¿Disfrutando de la voz\?</string>
<string name="review.feedback.button.no">Cancelar</string>
</resources>
</resources>
1 change: 1 addition & 0 deletions strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<string name="media_session_recent">Recent</string>
<string name="notification_sleep_timer_enable">Enable Sleep Timer</string>
<string name="notification_sleep_timer_disable">Disable Sleep Timer</string>
<string name="end_of_chapter">End of Chapter</string>
<string name="generic_error_message">Something went wrong 😢</string>
<string name="generic_error_retry">Retry</string>
<string name="storage_bug_title">Permission required</string>
Expand Down

0 comments on commit e6f5a5a

Please sign in to comment.