feat: Implement alarm playback when timer is complete

Closes: #15
This commit is contained in:
Nishant Mishra
2025-09-08 11:00:06 +05:30
parent ad8929217b
commit 99cbb46dca
6 changed files with 129 additions and 4 deletions

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.timerScreen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlarmDialog(
modifier: Modifier = Modifier,
stopAlarm: () -> Unit
) {
BasicAlertDialog(
onDismissRequest = stopAlarm,
modifier = modifier
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.clickable(onClick = stopAlarm),
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
Column(modifier = Modifier.padding(24.dp)) {
Icon(
painter = painterResource(R.drawable.alarm),
contentDescription = "Alarm",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(Modifier.height(16.dp))
Text(
text = "Stop Alarm?",
style = typography.headlineSmall,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(Modifier.height(16.dp))
Text(
text = "Current timer session is complete. Tap anywhere to stop the alarm."
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = stopAlarm,
modifier = Modifier.align(Alignment.End),
) {
Text("Ok")
}
}
}
}
}

View File

@@ -111,6 +111,9 @@ fun TimerScreen(
onResult = {} onResult = {}
) )
if (timerState.alarmRinging)
AlarmDialog { onAction(TimerAction.StopAlarm) }
Column(modifier = modifier) { Column(modifier = modifier) {
TopAppBar( TopAppBar(
title = { title = {

View File

@@ -10,5 +10,6 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel
sealed interface TimerAction { sealed interface TimerAction {
data object ResetTimer : TimerAction data object ResetTimer : TimerAction
data object SkipTimer : TimerAction data object SkipTimer : TimerAction
data object StopAlarm : TimerAction
data object ToggleTimer : TimerAction data object ToggleTimer : TimerAction
} }

View File

@@ -16,7 +16,8 @@ data class TimerState(
val nextTimeStr: String = "5:00", val nextTimeStr: String = "5:00",
val showBrandTitle: Boolean = true, val showBrandTitle: Boolean = true,
val currentFocusCount: Int = 1, val currentFocusCount: Int = 1,
val totalFocusCount: Int = 4 val totalFocusCount: Int = 4,
val alarmRinging: Boolean = false
) )
enum class TimerMode { enum class TimerMode {

View File

@@ -8,19 +8,23 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.media.MediaPlayer
import android.os.SystemClock import android.os.SystemClock
import android.provider.Settings
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.application
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
@@ -46,12 +50,13 @@ import kotlin.text.Typography.middleDot
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class TimerViewModel( class TimerViewModel(
application: Application,
private val preferenceRepository: PreferenceRepository, private val preferenceRepository: PreferenceRepository,
private val statRepository: StatRepository, private val statRepository: StatRepository,
private val timerRepository: TimerRepository, private val timerRepository: TimerRepository,
private val notificationBuilder: NotificationCompat.Builder, private val notificationBuilder: NotificationCompat.Builder,
private val notificationManager: NotificationManagerCompat private val notificationManager: NotificationManagerCompat
) : ViewModel() { ) : AndroidViewModel(application) {
private val _timerState = MutableStateFlow( private val _timerState = MutableStateFlow(
TimerState( TimerState(
totalTime = timerRepository.focusTime, totalTime = timerRepository.focusTime,
@@ -73,6 +78,11 @@ class TimerViewModel(
private lateinit var cs: ColorScheme private lateinit var cs: ColorScheme
private val alarm = MediaPlayer.create(
this.application,
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
)
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime = timerRepository.focusTime =
@@ -126,6 +136,7 @@ class TimerViewModel(
when (action) { when (action) {
TimerAction.ResetTimer -> resetTimer() TimerAction.ResetTimer -> resetTimer()
TimerAction.SkipTimer -> skipTimer() TimerAction.SkipTimer -> skipTimer()
TimerAction.StopAlarm -> stopAlarm()
TimerAction.ToggleTimer -> toggleTimer() TimerAction.ToggleTimer -> toggleTimer()
} }
} }
@@ -331,9 +342,24 @@ class TimerViewModel(
) )
.setShowWhen(true) .setShowWhen(true)
.setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time
.setSilent(!complete) .setSilent(true)
.build() .build()
) )
if (complete) {
alarm.start()
_timerState.update { currentState ->
currentState.copy(alarmRinging = true)
}
}
}
fun stopAlarm() {
alarm.pause()
alarm.seekTo(0)
_timerState.update { currentState ->
currentState.copy(alarmRinging = false)
}
} }
companion object { companion object {
@@ -374,6 +400,7 @@ class TimerViewModel(
.setOngoing(true) .setOngoing(true)
TimerViewModel( TimerViewModel(
application = application,
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
statRepository = appStatRepository, statRepository = appStatRepository,
timerRepository = appTimerRepository, timerRepository = appTimerRepository,

View File

@@ -0,0 +1,16 @@
<!--
~ Copyright (c) 2025 Nishant Mishra
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M520,504v-144q0,-17 -11.5,-28.5T480,320q-17,0 -28.5,11.5T440,360v159q0,8 3,15.5t9,13.5l112,112q11,11 28,11t28,-11q11,-11 11,-28t-11,-28L520,504ZM480,880q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,520q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,160q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,520q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,880ZM82,292q-11,-11 -11,-28t11,-28l114,-114q11,-11 28,-11t28,11q11,11 11,28t-11,28L138,292q-11,11 -28,11t-28,-11ZM878,292q-11,11 -28,11t-28,-11L708,178q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l114,114q11,11 11,28t-11,28Z" />
</vector>