mediaplayer out of view

This commit is contained in:
Rune Dyselinck 2023-04-28 13:38:17 +02:00
commit 46d60e100b
16 changed files with 326 additions and 271 deletions

View file

@ -3,19 +3,27 @@ package be.ugent.sel.studeez.data.local.models.timer_functional
class FunctionalCustomTimer(studyTime: Int) : FunctionalTimer(studyTime) {
override fun tick() {
if (time.time == 0) {
view = StudyState.DONE
} else {
if (!hasEnded()) {
time.minOne()
} else {
mediaPlayer?.setOnCompletionListener {
mediaPlayer!!.release()
mediaPlayer = null
}
mediaPlayer?.start()
}
}
override fun hasEnded(): Boolean {
return view == StudyState.DONE
}
override fun hasCurrentCountdownEnded(): Boolean {
return time.time == 0
}
override fun hasCurrentCountdownEnded(): Boolean {
return hasEnded()
}
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
return visitor.visitFunctionalCustomTimer(this)
}
}

View file

@ -1,6 +1,6 @@
package be.ugent.sel.studeez.data.local.models.timer_functional
class FunctionalEndlessTimer() : FunctionalTimer(0) {
class FunctionalEndlessTimer : FunctionalTimer(0) {
override fun hasEnded(): Boolean {
return false
@ -13,4 +13,8 @@ class FunctionalEndlessTimer() : FunctionalTimer(0) {
override fun tick() {
time.plusOne()
}
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
return visitor.visitFunctionalEndlessTimer(this)
}
}

View file

@ -9,18 +9,19 @@ class FunctionalPomodoroTimer(
var isInBreak = false
override fun tick() {
if (time.time == 0 && breaksRemaining == 0) {
view = StudyState.DONE
if (hasEnded()) {
mediaPlayer?.setOnCompletionListener {
mediaPlayer!!.release()
mediaPlayer = null
}
mediaPlayer?.start()
return
}
if (time.time == 0) {
} else if (hasCurrentCountdownEnded()) {
mediaPlayer?.start()
if (isInBreak) {
breaksRemaining--
view = StudyState.FOCUS_REMAINING
time.time = studyTime
} else {
view = StudyState.BREAK
time.time = breakTime
}
isInBreak = !isInBreak
@ -29,10 +30,18 @@ class FunctionalPomodoroTimer(
}
override fun hasEnded(): Boolean {
return breaksRemaining == 0 && time.time == 0
return !hasBreaksRemaining() && hasCurrentCountdownEnded()
}
private fun hasBreaksRemaining(): Boolean {
return breaksRemaining > 0
}
override fun hasCurrentCountdownEnded(): Boolean {
return time.time == 0
}
override fun <T> accept(visitor: FunctionalTimerVisitor<T>): T {
return visitor.visitFunctionalBreakTimer(this)
}
}

View file

@ -1,8 +1,10 @@
package be.ugent.sel.studeez.data.local.models.timer_functional
import android.media.MediaPlayer
abstract class FunctionalTimer(initialValue: Int) {
var time: Time = Time(initialValue)
var view: StudyState = StudyState.FOCUS
val time: Time = Time(initialValue)
var mediaPlayer: MediaPlayer? = null
fun getHoursMinutesSeconds(): HoursMinutesSeconds {
return time.getAsHMS()
@ -14,8 +16,5 @@ abstract class FunctionalTimer(initialValue: Int) {
abstract fun hasCurrentCountdownEnded(): Boolean
enum class StudyState {
FOCUS, DONE, BREAK, FOCUS_REMAINING
}
abstract fun <T> accept(visitor: FunctionalTimerVisitor<T>): T
}

View file

@ -0,0 +1,11 @@
package be.ugent.sel.studeez.data.local.models.timer_functional
interface FunctionalTimerVisitor<T> {
fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): T
fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): T
fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): T
}

View file

@ -1,6 +1,5 @@
package be.ugent.sel.studeez.screens.session
import android.media.MediaPlayer
import kotlinx.coroutines.delay
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds
@ -8,11 +7,9 @@ import kotlin.time.Duration.Companion.seconds
@Singleton
object InvisibleSessionManager {
private var viewModel: SessionViewModel? = null
private var mediaplayer: MediaPlayer? = null
fun setParameters(viewModel: SessionViewModel, mediaplayer: MediaPlayer) {
fun setParameters(viewModel: SessionViewModel) {
this.viewModel = viewModel
this.mediaplayer = mediaplayer
}
suspend fun updateTimer() {
@ -20,15 +17,7 @@ object InvisibleSessionManager {
while (true) {
delay(1.seconds)
viewModel!!.getTimer().tick()
if (viewModel!!.getTimer().hasCurrentCountdownEnded() && !viewModel!!.getTimer().hasEnded()) {
mediaplayer?.start()
}
}
}
}
fun removeParameters() {
viewModel = null
mediaplayer = null
}
}

View file

@ -0,0 +1,53 @@
package be.ugent.sel.studeez.screens.session
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.screens.session.sessionScreens.AbstractSessionScreen
import be.ugent.sel.studeez.screens.session.sessionScreens.GetSessionScreen
data class SessionActions(
val getTimer: () -> FunctionalTimer,
val getTask: () -> String,
val startMediaPlayer: () -> Unit,
val releaseMediaPlayer: () -> Unit,
)
private fun getSessionActions(
viewModel: SessionViewModel,
mediaplayer: MediaPlayer,
): SessionActions {
return SessionActions(
getTimer = viewModel::getTimer,
getTask = viewModel::getTask,
startMediaPlayer = mediaplayer::start,
releaseMediaPlayer = mediaplayer::release,
)
}
@Composable
fun SessionRoute(
open: (String) -> Unit,
viewModel: SessionViewModel,
) {
val context = LocalContext.current
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val mediaplayer = MediaPlayer.create(context, uri)
mediaplayer.isLooping = false
viewModel.getTimer().mediaPlayer = mediaplayer
InvisibleSessionManager.setParameters(
viewModel = viewModel
)
val sessionScreen: AbstractSessionScreen = viewModel.getTimer().accept(GetSessionScreen())
sessionScreen(
open = open,
sessionActions = getSessionActions(viewModel, mediaplayer)
)
}

View file

@ -1,206 +0,0 @@
package be.ugent.sel.studeez.screens.session
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.ui.Alignment
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import be.ugent.sel.studeez.navigation.StudeezDestinations
import androidx.compose.ui.unit.sp
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer.StudyState
import be.ugent.sel.studeez.resources
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
data class SessionActions(
val getTimer: () -> FunctionalTimer,
val getTask: () -> String,
val releaseMediaPlayer: () -> Unit,
val startMediaPlayer: () -> Unit,
)
fun getSessionActions(
viewModel: SessionViewModel,
mediaplayer: MediaPlayer,
): SessionActions {
return SessionActions(
getTimer = viewModel::getTimer,
getTask = viewModel::getTask,
releaseMediaPlayer = mediaplayer::release,
startMediaPlayer = mediaplayer::start,
)
}
@Composable
fun SessionRoute(
open: (String) -> Unit,
viewModel: SessionViewModel,
) {
val context = LocalContext.current
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val mediaplayer = MediaPlayer.create(context, uri)
mediaplayer.isLooping = false
InvisibleSessionManager.setParameters(
viewModel = viewModel,
mediaplayer = mediaplayer
)
SessionScreen(
open = open,
sessionActions = getSessionActions(viewModel, mediaplayer)
)
}
@Composable
fun SessionScreen(
open: (String) -> Unit,
sessionActions: SessionActions
) {
Column(
modifier = Modifier.padding(10.dp)
) {
Timer(
sessionActions = sessionActions
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
TextButton(
onClick = {
sessionActions.releaseMediaPlayer
InvisibleSessionManager.removeParameters()
open(StudeezDestinations.HOME_SCREEN)
// Vanaf hier ook naar report gaan als "end session" knop word ingedrukt
},
modifier = Modifier
.padding(horizontal = 20.dp)
.border(1.dp, Color.Red, RoundedCornerShape(32.dp))
.background(Color.Transparent)
) {
Text(
text = resources().getString(R.string.end_session),
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier
.padding(1.dp)
)
}
}
}
}
@Composable
private fun Timer(
sessionActions: SessionActions
) {
var tikker by remember { mutableStateOf(false) }
LaunchedEffect(tikker) {
delay(1.seconds)
sessionActions.getTimer().tick()
tikker = !tikker
}
if (sessionActions.getTimer().hasCurrentCountdownEnded() && !sessionActions.getTimer().hasEnded()) {
sessionActions.startMediaPlayer
}
val hms = sessionActions.getTimer().getHoursMinutesSeconds()
Column {
Text(
text = "${hms.hours} : ${hms.minutes} : ${hms.seconds}",
modifier = Modifier
.fillMaxWidth()
.padding(50.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 40.sp,
)
val stateString: String = when (sessionActions.getTimer().view) {
StudyState.DONE -> resources().getString(R.string.state_done)
StudyState.FOCUS -> resources().getString(R.string.state_focus)
StudyState.BREAK -> resources().getString(R.string.state_take_a_break)
StudyState.FOCUS_REMAINING ->
(sessionActions.getTimer() as FunctionalPomodoroTimer?)?.breaksRemaining?.let {
resources().getQuantityString(R.plurals.state_focus_remaining, it, it)
}.toString()
}
Text(
text = stateString,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
fontSize = 30.sp
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color.Blue, RoundedCornerShape(32.dp))
) {
Text(
text = sessionActions.getTask(),
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 20.dp)
)
}
}
}
}
@Preview
@Composable
fun TimerPreview() {
Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}))
}
@Preview
@Composable
fun SessionPreview() {
SessionScreen(
open = {},
sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {})
)
}

View file

@ -0,0 +1,136 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.navigation.StudeezDestinations
import be.ugent.sel.studeez.screens.session.SessionActions
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
abstract class AbstractSessionScreen {
@Composable
operator fun invoke(
open: (String) -> Unit,
sessionActions: SessionActions,
) {
Column(
modifier = Modifier.padding(10.dp)
) {
Timer(
sessionActions = sessionActions,
)
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
TextButton(
onClick = {
sessionActions.releaseMediaPlayer
open(StudeezDestinations.HOME_SCREEN)
// Vanaf hier ook naar report gaan als "end session" knop word ingedrukt
},
modifier = Modifier
.padding(horizontal = 20.dp)
.border(1.dp, Color.Red, RoundedCornerShape(32.dp))
.background(Color.Transparent)
) {
Text(
text = "End session",
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier.padding(1.dp)
)
}
}
}
}
@Composable
fun Timer(
sessionActions: SessionActions,
) {
var tikker by remember { mutableStateOf(false) }
LaunchedEffect(tikker) {
delay(1.seconds)
sessionActions.getTimer().tick()
tikker = !tikker
}
val hms = sessionActions.getTimer().getHoursMinutesSeconds()
Column {
Text(
text = "${hms.hours} : ${hms.minutes} : ${hms.seconds}",
modifier = Modifier
.fillMaxWidth()
.padding(50.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 40.sp,
)
Text(
text = motivationString(),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
fontSize = 30.sp
)
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.fillMaxWidth()
.padding(50.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color.Blue, RoundedCornerShape(32.dp))
) {
Text(
text = sessionActions.getTask(),
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 20.dp)
)
}
}
}
}
@Composable
abstract fun motivationString(): String
}
@Preview
@Composable
fun TimerPreview() {
val sessionScreen = object : AbstractSessionScreen() {
@Composable
override fun motivationString(): String = "Test"
}
sessionScreen.Timer(sessionActions = SessionActions({ FunctionalEndlessTimer() }, { "Preview" }, {}, {}))
}

View file

@ -0,0 +1,30 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.R
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class BreakSessionScreen(
private val funPomoDoroTimer: FunctionalPomodoroTimer
): AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
if (funPomoDoroTimer.isInBreak) {
return resources().getString(AppText.state_take_a_break)
}
if (funPomoDoroTimer.hasEnded()) {
return resources().getString(AppText.state_done)
}
return resources().getQuantityString(
R.plurals.state_focus_remaining,
funPomoDoroTimer.breaksRemaining,
funPomoDoroTimer.breaksRemaining
)
}
}

View file

@ -0,0 +1,21 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class CustomSessionScreen(
private val functionalTimer: FunctionalCustomTimer
): AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
if (functionalTimer.hasEnded()) {
return resources().getString(AppText.state_done)
}
return resources().getString(AppText.state_focus)
}
}

View file

@ -0,0 +1,14 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import androidx.compose.runtime.Composable
import be.ugent.sel.studeez.resources
import be.ugent.sel.studeez.R.string as AppText
class EndlessSessionScreen : AbstractSessionScreen() {
@Composable
override fun motivationString(): String {
return resources().getString(AppText.state_focus)
}
}

View file

@ -0,0 +1,17 @@
package be.ugent.sel.studeez.screens.session.sessionScreens
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimerVisitor
class GetSessionScreen : FunctionalTimerVisitor<AbstractSessionScreen> {
override fun visitFunctionalCustomTimer(functionalCustomTimer: FunctionalCustomTimer): AbstractSessionScreen =
CustomSessionScreen(functionalCustomTimer)
override fun visitFunctionalEndlessTimer(functionalEndlessTimer: FunctionalEndlessTimer): AbstractSessionScreen =
EndlessSessionScreen()
override fun visitFunctionalBreakTimer(functionalPomodoroTimer: FunctionalPomodoroTimer): AbstractSessionScreen =
BreakSessionScreen(functionalPomodoroTimer)
}

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalCustomTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
@ -36,9 +35,6 @@ class FunctionalCustomTimerUnitTest : FunctionalTimerUnitTest() {
timer = FunctionalCustomTimer(0)
timer.tick()
Assert.assertTrue(timer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.DONE,
timer.view
)
Assert.assertTrue(timer.hasEnded())
}
}

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalEndlessTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
@ -37,10 +36,6 @@ class FunctionalEndlessTimerUnitTest : FunctionalTimerUnitTest() {
for (i in 1..n) {
timer.tick()
Assert.assertFalse(timer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS,
timer.view
)
}
}
}

View file

@ -1,7 +1,6 @@
package be.ugent.sel.studeez.timer_functional
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalPomodoroTimer
import be.ugent.sel.studeez.data.local.models.timer_functional.FunctionalTimer
import org.junit.Assert
import org.junit.Test
@ -29,10 +28,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
breaks,
pomodoroTimer.breaksRemaining,
)
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS,
pomodoroTimer.view,
)
}
@Test
@ -52,10 +47,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
pomodoroTimer = FunctionalPomodoroTimer(0, 0, 0)
pomodoroTimer.tick()
Assert.assertTrue(pomodoroTimer.hasEnded())
Assert.assertEquals(
FunctionalTimer.StudyState.DONE,
pomodoroTimer.view,
)
}
@Test
@ -65,10 +56,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
}
Assert.assertFalse(pomodoroTimer.hasEnded())
Assert.assertTrue(pomodoroTimer.isInBreak)
Assert.assertEquals(
FunctionalTimer.StudyState.BREAK,
pomodoroTimer.view
)
}
@Test
@ -77,10 +64,6 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
pomodoroTimer.tick()
}
Assert.assertTrue(pomodoroTimer.isInBreak)
Assert.assertEquals(
FunctionalTimer.StudyState.BREAK,
pomodoroTimer.view
)
for (i in 0..breakTime) {
pomodoroTimer.tick()
}
@ -90,9 +73,5 @@ class FunctionalPomodoroTimerUnitTest : FunctionalTimerUnitTest() {
breaksRemaining,
pomodoroTimer.breaksRemaining
)
Assert.assertEquals(
FunctionalTimer.StudyState.FOCUS_REMAINING,
pomodoroTimer.view
)
}
}