feat: Implement a basic Progress-Centric notification

Permissions are not checked for now. This will be fixed in further commits
This commit is contained in:
Nishant Mishra
2025-08-20 19:52:45 +05:30
parent 51f469d722
commit 19c47dd697
2 changed files with 103 additions and 2 deletions

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".TomatoApplication" android:name=".TomatoApplication"
android:allowBackup="true" android:allowBackup="true"

View File

@@ -7,7 +7,15 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.SystemClock import android.os.SystemClock
import androidx.annotation.RequiresPermission
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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -23,6 +31,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.PreferenceRepository import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.Stat
@@ -30,12 +39,16 @@ import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.utils.millisecondsToStr import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate import java.time.LocalDate
import kotlin.math.ceil
import kotlin.text.Typography.middleDot
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class TimerViewModel( class TimerViewModel(
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 notificationManager: NotificationManagerCompat
) : ViewModel() { ) : ViewModel() {
// TODO: Document code // TODO: Document code
private val _timerState = MutableStateFlow( private val _timerState = MutableStateFlow(
@@ -134,6 +147,7 @@ class TimerViewModel(
private fun skipTimer() { private fun skipTimer() {
viewModelScope.launch { viewModelScope.launch {
saveTimeToDb() saveTimeToDb()
showTimerNotification(0, paused = true, complete = true)
startTime = 0L startTime = 0L
pauseTime = 0L pauseTime = 0L
pauseDuration = 0L pauseDuration = 0L
@@ -174,6 +188,7 @@ class TimerViewModel(
private fun toggleTimer() { private fun toggleTimer() {
if (timerState.value.timerRunning) { if (timerState.value.timerRunning) {
showTimerNotification(time.value.toInt(), paused = true)
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy(timerRunning = false) currentState.copy(timerRunning = false)
} }
@@ -183,6 +198,8 @@ class TimerViewModel(
_timerState.update { it.copy(timerRunning = true) } _timerState.update { it.copy(timerRunning = true) }
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
var iterations = -1
timerJob = viewModelScope.launch { timerJob = viewModelScope.launch {
while (true) { while (true) {
if (!timerState.value.timerRunning) break if (!timerState.value.timerRunning) break
@@ -201,6 +218,10 @@ class TimerViewModel(
} }
} }
iterations = (iterations + 1) % 50
if (iterations == 0) showTimerNotification(time.value.toInt())
if (time.value < 0) { if (time.value < 0) {
skipTimer() skipTimer()
@@ -232,6 +253,67 @@ class TimerViewModel(
} }
} }
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
fun showTimerNotification(
remainingTime: Int,
paused: Boolean = false,
complete: Boolean = false
) {
val totalTime = when (timerState.value.timerMode) {
TimerMode.FOCUS -> timerRepository.focusTime.toInt()
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
else -> timerRepository.longBreakTime.toInt()
}
val currentTimer = when (timerState.value.timerMode) {
TimerMode.FOCUS -> "Focus"
TimerMode.SHORT_BREAK -> "Short break"
else -> "Long break"
}
val nextTimer = when (timerState.value.nextTimerMode) {
TimerMode.FOCUS -> "Focus"
TimerMode.SHORT_BREAK -> "Short break"
else -> "Long break"
}
notificationManager.notify(
1,
notificationBuilder
.setContentTitle("$currentTimer $middleDot ${ceil(remainingTime.toFloat() / 60000f).toInt()} min remaining")
.setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})")
.setStyle(
NotificationCompat.ProgressStyle()
.also {
for (i in 0..<timerRepository.sessionLength * 2) {
if (i % 2 == 0) it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment(
timerRepository.focusTime.toInt()
)
)
else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment(timerRepository.shortBreakTime.toInt())
)
else it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment(
timerRepository.longBreakTime.toInt()
)
)
}
}
.setProgress(
(totalTime - remainingTime) +
((cycles + 1) / 2) * timerRepository.focusTime.toInt() +
(cycles / 2) * timerRepository.shortBreakTime.toInt()
)
)
.setShowWhen(true)
.setShortCriticalText("${remainingTime / 60000} min")
.setSilent(true)
.build()
)
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
@@ -240,10 +322,27 @@ class TimerViewModel(
val appStatRepository = application.container.appStatRepository val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository val appTimerRepository = application.container.appTimerRepository
val notificationManager = NotificationManagerCompat.from(application)
val notificationChannel = NotificationChannel(
"timer",
"Timer progress",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
val notificationBuilder = NotificationCompat.Builder(application, "timer")
.setSmallIcon(R.drawable.hourglass_filled)
.setOngoing(true)
.setColor(Color.Red.toArgb())
TimerViewModel( TimerViewModel(
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
statRepository = appStatRepository, statRepository = appStatRepository,
timerRepository = appTimerRepository timerRepository = appTimerRepository,
notificationBuilder = notificationBuilder,
notificationManager = notificationManager
) )
} }
} }