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 = {}
)
if (timerState.alarmRinging)
AlarmDialog { onAction(TimerAction.StopAlarm) }
Column(modifier = modifier) {
TopAppBar(
title = {

View File

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

View File

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

View File

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