From 90fa94e06509fb14875ac6d6ca0885a0876a039b Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 12:39:27 +0530 Subject: [PATCH 1/7] feat: Implement a foreground service to run the timer This fixes many issues that were occurring because of the app being killed in the background Closes: #31 --- app/src/main/AndroidManifest.xml | 9 + .../org/nsh07/pomodoro/TomatoApplication.kt | 10 + .../org/nsh07/pomodoro/data/AppContainer.kt | 39 ++ .../nsh07/pomodoro/service/TimerService.kt | 346 ++++++++++++++++++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 40 +- .../timerScreen/viewModel/TimerViewModel.kt | 269 +------------- 6 files changed, 448 insertions(+), 265 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9dc04d4..a5372a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + @@ -26,6 +28,13 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt index b0e144f..e1902bc 100644 --- a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt +++ b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt @@ -1,6 +1,8 @@ package org.nsh07.pomodoro import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager import org.nsh07.pomodoro.data.AppContainer import org.nsh07.pomodoro.data.DefaultAppContainer @@ -9,5 +11,13 @@ class TomatoApplication : Application() { override fun onCreate() { super.onCreate() container = DefaultAppContainer(this) + + val notificationChannel = NotificationChannel( + "timer", + "Timer progress", + NotificationManager.IMPORTANCE_HIGH + ) + + container.notificationManager.createNotificationChannel(notificationChannel) } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 2d45910..51c3570 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -8,11 +8,23 @@ package org.nsh07.pomodoro.data import android.content.Context +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import kotlinx.coroutines.flow.MutableStateFlow +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState +import org.nsh07.pomodoro.utils.millisecondsToStr interface AppContainer { val appPreferenceRepository: AppPreferenceRepository val appStatRepository: AppStatRepository val appTimerRepository: AppTimerRepository + val notificationManager: NotificationManagerCompat + val notificationBuilder: NotificationCompat.Builder + val timerState: MutableStateFlow + val time: MutableStateFlow } class DefaultAppContainer(context: Context) : AppContainer { @@ -27,4 +39,31 @@ class DefaultAppContainer(context: Context) : AppContainer { override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() } + override val notificationManager: NotificationManagerCompat by lazy { + NotificationManagerCompat.from(context) + } + + override val notificationBuilder: NotificationCompat.Builder by lazy { + NotificationCompat.Builder(context, "timer") + .setSmallIcon(R.drawable.tomato_logo_notification) + .setOngoing(true) + .setColor(Color.Red.toArgb()) + .setRequestPromotedOngoing(true) + .setOngoing(true) + } + + override val timerState: MutableStateFlow by lazy { + MutableStateFlow( + TimerState( + totalTime = appTimerRepository.focusTime, + timeStr = millisecondsToStr(appTimerRepository.focusTime), + nextTimeStr = millisecondsToStr(appTimerRepository.shortBreakTime) + ) + ) + } + + override val time: MutableStateFlow by lazy { + MutableStateFlow(appTimerRepository.focusTime) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt new file mode 100644 index 0000000..9e94929 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -0,0 +1,346 @@ +package org.nsh07.pomodoro.service + +import android.annotation.SuppressLint +import android.app.Service +import android.content.Intent +import android.media.MediaPlayer +import android.os.IBinder +import android.os.SystemClock +import android.provider.Settings +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.toArgb +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.nsh07.pomodoro.TomatoApplication +import org.nsh07.pomodoro.data.AppContainer +import org.nsh07.pomodoro.data.StatRepository +import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState +import org.nsh07.pomodoro.utils.millisecondsToStr +import kotlin.text.Typography.middleDot + +@ExperimentalAnimationApi +class TimerService : Service() { + private lateinit var appContainer: AppContainer + + private lateinit var timerRepository: TimerRepository + private lateinit var statRepository: StatRepository + private lateinit var notificationManager: NotificationManagerCompat + private lateinit var notificationBuilder: NotificationCompat.Builder + private lateinit var _timerState: MutableStateFlow + private lateinit var _time: MutableStateFlow + + val timeStateFlow by lazy { + _time.asStateFlow() + } + + var time: Long + get() = timeStateFlow.value + set(value) = _time.update { value } + + lateinit var timerState: StateFlow + + private var cycles = 0 + private var startTime = 0L + private var pauseTime = 0L + private var pauseDuration = 0L + + private val timerJob = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + timerJob) + private val skipScope = CoroutineScope(Dispatchers.IO) + + private lateinit var alarm: MediaPlayer + + private var cs: ColorScheme = lightColorScheme() + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onCreate() { + appContainer = (application as TomatoApplication).container + timerRepository = appContainer.appTimerRepository + statRepository = appContainer.appStatRepository + notificationManager = NotificationManagerCompat.from(this) + notificationBuilder = appContainer.notificationBuilder + _timerState = appContainer.timerState + _time = appContainer.time + + timerState = _timerState.asStateFlow() + + alarm = MediaPlayer.create( + this, + Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI + ) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + Actions.TOGGLE.toString() -> { + startForegroundService() + toggleTimer() + } + + Actions.RESET.toString() -> { + if (timerState.value.timerRunning) toggleTimer() + resetTimer() + stopForegroundService() + } + + Actions.SKIP.toString() -> skipTimer(true) + + Actions.STOP_ALARM.toString() -> stopAlarm() + } + return super.onStartCommand(intent, flags, startId) + } + + private fun toggleTimer() { + if (timerState.value.timerRunning) { + showTimerNotification(time.toInt(), paused = true) + _timerState.update { currentState -> + currentState.copy(timerRunning = false) + } + pauseTime = SystemClock.elapsedRealtime() + } else { + _timerState.update { it.copy(timerRunning = true) } + if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime + + var iterations = -1 + + scope.launch { + while (true) { + if (!timerState.value.timerRunning) break + if (startTime == 0L) startTime = SystemClock.elapsedRealtime() + + time = when (timerState.value.timerMode) { + TimerMode.FOCUS -> timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + + TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + + else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() + } + + iterations = (iterations + 1) % 50 + + if (iterations == 0) showTimerNotification(time.toInt()) + + if (time < 0) { + skipTimer() + + _timerState.update { currentState -> + currentState.copy(timerRunning = false) + } + timerJob.cancel() + } else { + _timerState.update { currentState -> + currentState.copy( + timeStr = millisecondsToStr(time) + ) + } + } + + delay(100) + } + } + } + } + + @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI + 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" + } + + val remainingTimeString = if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1" + else (remainingTime.toFloat() / 60000f).toInt() + + notificationManager.notify( + 1, + notificationBuilder + .setContentTitle( + if (!complete) { + "$currentTimer $middleDot $remainingTimeString min remaining" + if (paused) " $middleDot Paused" else "" + } else "$currentTimer $middleDot Completed" + ) + .setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})") + .setStyle( + NotificationCompat.ProgressStyle().also { + // Add all the Focus, Short break and long break intervals in order + for (i in 0.. + currentState.copy(alarmRinging = true) + } + } + } + + private fun resetTimer() { + skipScope.launch { + saveTimeToDb() + time = timerRepository.focusTime + cycles = 0 + startTime = 0L + pauseTime = 0L + pauseDuration = 0L + + _timerState.update { currentState -> + currentState.copy( + timerMode = TimerMode.FOCUS, + timeStr = millisecondsToStr(time), + totalTime = time, + nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, + nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), + currentFocusCount = 1, + totalFocusCount = timerRepository.sessionLength + ) + } + } + } + + private fun skipTimer(fromButton: Boolean = false) { + skipScope.launch { + saveTimeToDb() + showTimerNotification(0, paused = true, complete = !fromButton) + startTime = 0L + pauseTime = 0L + pauseDuration = 0L + + cycles = (cycles + 1) % (timerRepository.sessionLength * 2) + + if (cycles % 2 == 0) { + time = timerRepository.focusTime + _timerState.update { currentState -> + currentState.copy( + timerMode = TimerMode.FOCUS, + timeStr = millisecondsToStr(time), + totalTime = time, + nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, + nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( + timerRepository.longBreakTime + ) else millisecondsToStr( + timerRepository.shortBreakTime + ), + currentFocusCount = cycles / 2 + 1, + totalFocusCount = timerRepository.sessionLength + ) + } + } else { + val long = cycles == (timerRepository.sessionLength * 2) - 1 + time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime + + _timerState.update { currentState -> + currentState.copy( + timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, + timeStr = millisecondsToStr(time), + totalTime = time, + nextTimerMode = TimerMode.FOCUS, + nextTimeStr = millisecondsToStr(timerRepository.focusTime) + ) + } + } + } + } + + fun stopAlarm() { + alarm.pause() + alarm.seekTo(0) + _timerState.update { currentState -> + currentState.copy(alarmRinging = false) + } + } + + suspend fun saveTimeToDb() { + when (timerState.value.timerMode) { + TimerMode.FOCUS -> statRepository.addFocusTime( + (timerState.value.totalTime - time).coerceAtLeast( + 0L + ) + ) + + else -> statRepository.addBreakTime((timerState.value.totalTime - time).coerceAtLeast(0L)) + } + } + + private fun startForegroundService() { + startForeground(1, notificationBuilder.build()) + } + + private fun stopForegroundService() { + notificationManager.cancel(1) + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + + private fun setStopButton() { + // TODO + } + + private fun setResumeButton() { + // TODO + } + + override fun onDestroy() { + super.onDestroy() + timerJob.cancel() + } + + enum class Actions { + TOGGLE, SKIP, RESET, STOP_ALARM + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index dadfe37..24914b1 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -7,8 +7,10 @@ package org.nsh07.pomodoro.ui +import android.content.Intent import androidx.compose.animation.ContentTransform import androidx.compose.animation.Crossfade +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleOut @@ -33,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource @@ -44,19 +47,26 @@ import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import androidx.window.core.layout.WindowSizeClass import org.nsh07.pomodoro.MainActivity.Companion.screens +import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.timerScreen.TimerScreen +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class, + ExperimentalAnimationApi::class +) @Composable fun AppScreen( modifier: Modifier = Modifier, timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory), statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) ) { + val context = LocalContext.current + val uiState by timerViewModel.timerState.collectAsStateWithLifecycle() val remainingTime by timerViewModel.time.collectAsStateWithLifecycle() @@ -139,7 +149,33 @@ fun AppScreen( TimerScreen( timerState = uiState, progress = { progress }, - onAction = timerViewModel::onAction, + onAction = { action -> + when (action) { + TimerAction.ResetTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.RESET.toString() + context.startService(it) + } + + is TimerAction.SkipTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.SKIP.toString() + context.startService(it) + } + + TimerAction.StopAlarm -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.STOP_ALARM.toString() + context.startService(it) + } + + TimerAction.ToggleTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + context.startService(it) + } + } + }, modifier = modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index ebfd45e..a173dd4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -7,38 +7,22 @@ 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.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 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.nsh07.pomodoro.MainActivity -import org.nsh07.pomodoro.R import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.PreferenceRepository import org.nsh07.pomodoro.data.Stat @@ -46,7 +30,6 @@ import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.utils.millisecondsToStr import java.time.LocalDate -import kotlin.text.Typography.middleDot @OptIn(FlowPreview::class) class TimerViewModel( @@ -54,20 +37,10 @@ class TimerViewModel( private val preferenceRepository: PreferenceRepository, private val statRepository: StatRepository, private val timerRepository: TimerRepository, - private val notificationBuilder: NotificationCompat.Builder, - private val notificationManager: NotificationManagerCompat + private val _timerState: MutableStateFlow, + private val _time: MutableStateFlow ) : AndroidViewModel(application) { - private val _timerState = MutableStateFlow( - TimerState( - totalTime = timerRepository.focusTime, - timeStr = millisecondsToStr(timerRepository.focusTime), - nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime) - ) - ) - val timerState: StateFlow = _timerState.asStateFlow() - var timerJob: Job? = null - private val _time = MutableStateFlow(timerRepository.focusTime) val time: StateFlow = _time.asStateFlow() private var cycles = 0 @@ -78,11 +51,6 @@ 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 = @@ -132,16 +100,6 @@ class TimerViewModel( cs = colorScheme } - fun onAction(action: TimerAction) { - when (action) { - is TimerAction.SkipTimer -> skipTimer(action.fromButton) - - TimerAction.ResetTimer -> resetTimer() - TimerAction.StopAlarm -> stopAlarm() - TimerAction.ToggleTimer -> toggleTimer() - } - } - private fun resetTimer() { viewModelScope.launch { saveTimeToDb() @@ -165,107 +123,6 @@ class TimerViewModel( } } - private fun skipTimer(fromButton: Boolean = false) { - viewModelScope.launch { - saveTimeToDb() - showTimerNotification(0, paused = true, complete = !fromButton) - startTime = 0L - pauseTime = 0L - pauseDuration = 0L - - cycles = (cycles + 1) % (timerRepository.sessionLength * 2) - - if (cycles % 2 == 0) { - _time.update { timerRepository.focusTime } - _timerState.update { currentState -> - currentState.copy( - timerMode = TimerMode.FOCUS, - timeStr = millisecondsToStr(time.value), - totalTime = time.value, - nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, - nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( - timerRepository.longBreakTime - ) else millisecondsToStr( - timerRepository.shortBreakTime - ), - currentFocusCount = cycles / 2 + 1, - totalFocusCount = timerRepository.sessionLength - ) - } - } else { - val long = cycles == (timerRepository.sessionLength * 2) - 1 - _time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime } - - _timerState.update { currentState -> - currentState.copy( - timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, - timeStr = millisecondsToStr(time.value), - totalTime = time.value, - nextTimerMode = TimerMode.FOCUS, - nextTimeStr = millisecondsToStr(timerRepository.focusTime) - ) - } - } - } - } - - private fun toggleTimer() { - if (timerState.value.timerRunning) { - showTimerNotification(time.value.toInt(), paused = true) - _timerState.update { currentState -> - currentState.copy(timerRunning = false) - } - timerJob?.cancel() - pauseTime = SystemClock.elapsedRealtime() - } else { - _timerState.update { it.copy(timerRunning = true) } - if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime - - var iterations = -1 - - timerJob = viewModelScope.launch { - while (true) { - if (!timerState.value.timerRunning) break - if (startTime == 0L) startTime = SystemClock.elapsedRealtime() - - _time.update { - when (timerState.value.timerMode) { - TimerMode.FOCUS -> - timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() - - TimerMode.SHORT_BREAK -> - timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() - - else -> - timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() - } - } - - iterations = (iterations + 1) % 50 - - if (iterations == 0) showTimerNotification(time.value.toInt()) - - if (time.value < 0) { - skipTimer() - - _timerState.update { currentState -> - currentState.copy(timerRunning = false) - } - timerJob?.cancel() - } else { - _timerState.update { currentState -> - currentState.copy( - timeStr = millisecondsToStr(time.value) - ) - } - } - - delay(100) - } - } - } - } - suspend fun saveTimeToDb() { when (timerState.value.timerMode) { TimerMode.FOCUS -> statRepository @@ -276,93 +133,6 @@ class TimerViewModel( } } - @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI - 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" - } - - val remainingTimeString = - if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1" - else (remainingTime.toFloat() / 60000f).toInt() - - notificationManager.notify( - 1, - notificationBuilder - .setContentTitle( - if (!complete) { - "$currentTimer $middleDot $remainingTimeString min remaining" + if (paused) " $middleDot Paused" else "" - } else "$currentTimer $middleDot Completed" - ) - .setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})") - .setStyle( - NotificationCompat.ProgressStyle() - .also { - // Add all the Focus, Short break and long break intervals in order - for (i in 0.. - currentState.copy(alarmRinging = true) - } - } - } - - fun stopAlarm() { - alarm.pause() - alarm.seekTo(0) - _timerState.update { currentState -> - currentState.copy(alarmRinging = false) - } - } - companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -370,43 +140,16 @@ class TimerViewModel( val appPreferenceRepository = application.container.appPreferenceRepository val appStatRepository = application.container.appStatRepository val appTimerRepository = application.container.appTimerRepository - - val notificationManager = NotificationManagerCompat.from(application) - - val notificationChannel = NotificationChannel( - "timer", - "Timer progress", - NotificationManager.IMPORTANCE_HIGH - ) - - notificationManager.createNotificationChannel(notificationChannel) - - val openAppIntent = Intent(application, MainActivity::class.java) - openAppIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - openAppIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - - val contentIntent = PendingIntent.getActivity( - application, - System.currentTimeMillis().toInt(), - openAppIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val notificationBuilder = NotificationCompat.Builder(application, "timer") - .setSmallIcon(R.drawable.tomato_logo_notification) - .setOngoing(true) - .setColor(Color.Red.toArgb()) - .setContentIntent(contentIntent) - .setRequestPromotedOngoing(true) - .setOngoing(true) + val timerState = application.container.timerState + val time = application.container.time TimerViewModel( application = application, preferenceRepository = appPreferenceRepository, statRepository = appStatRepository, timerRepository = appTimerRepository, - notificationBuilder = notificationBuilder, - notificationManager = notificationManager + _timerState = timerState, + _time = time ) } } From fa984216f37976d179db38f3f8ead200c062de2b Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 12:52:21 +0530 Subject: [PATCH 2/7] fix: Save time to database when foreground service is closed Potentially fixes #10, I still need to test it though --- .../main/java/org/nsh07/pomodoro/data/AppContainer.kt | 5 +++-- .../main/java/org/nsh07/pomodoro/service/TimerService.kt | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 51c3570..7056373 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -46,10 +46,11 @@ class DefaultAppContainer(context: Context) : AppContainer { override val notificationBuilder: NotificationCompat.Builder by lazy { NotificationCompat.Builder(context, "timer") .setSmallIcon(R.drawable.tomato_logo_notification) - .setOngoing(true) .setColor(Color.Red.toArgb()) - .setRequestPromotedOngoing(true) + .setShowWhen(true) + .setSilent(true) .setOngoing(true) + .setRequestPromotedOngoing(true) } override val timerState: MutableStateFlow by lazy { diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 9e94929..976be41 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.AppContainer import org.nsh07.pomodoro.data.StatRepository @@ -216,9 +217,7 @@ class TimerService : Service() { (totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt() ) ) - .setShowWhen(true) .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time - .setSilent(true) .build() ) @@ -337,7 +336,11 @@ class TimerService : Service() { override fun onDestroy() { super.onDestroy() - timerJob.cancel() + runBlocking { + timerJob.cancel() + saveTimeToDb() + notificationManager.cancel(1) + } } enum class Actions { From 672571764d8a70e98d0b464dd0464a4b71817896 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 13:00:36 +0530 Subject: [PATCH 3/7] fix: Make app open when clicking notification --- .../main/java/org/nsh07/pomodoro/data/AppContainer.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 7056373..a90f7d5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -7,6 +7,7 @@ package org.nsh07.pomodoro.data +import android.app.PendingIntent import android.content.Context import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -47,6 +48,14 @@ class DefaultAppContainer(context: Context) : AppContainer { NotificationCompat.Builder(context, "timer") .setSmallIcon(R.drawable.tomato_logo_notification) .setColor(Color.Red.toArgb()) + .setContentIntent( + PendingIntent.getActivity( + context, + 0, + context.packageManager.getLaunchIntentForPackage(context.packageName), + PendingIntent.FLAG_IMMUTABLE + ) + ) .setShowWhen(true) .setSilent(true) .setOngoing(true) From 781c103f592fbcdf93f82d802fc79f6fa9044ba3 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 15:31:27 +0530 Subject: [PATCH 4/7] feat: Add notification icons to play, pause reset and skip the timer --- .../org/nsh07/pomodoro/data/AppContainer.kt | 2 + .../org/nsh07/pomodoro/service/AddActions.kt | 62 +++++++++++++++++++ .../nsh07/pomodoro/service/TimerService.kt | 18 +++--- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 6 +- 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index a90f7d5..2bde857 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -15,6 +15,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import kotlinx.coroutines.flow.MutableStateFlow import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.service.addTimerActions import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.utils.millisecondsToStr @@ -56,6 +57,7 @@ class DefaultAppContainer(context: Context) : AppContainer { PendingIntent.FLAG_IMMUTABLE ) ) + .addTimerActions(context, R.drawable.play, "Start") .setShowWhen(true) .setSilent(true) .setOngoing(true) diff --git a/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt b/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt new file mode 100644 index 0000000..5475c1e --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.service + +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.content.Context +import android.content.Intent +import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import org.nsh07.pomodoro.R + +fun NotificationCompat.Builder.addTimerActions( + context: Context, + @DrawableRes playPauseIcon: Int, + playPauseText: String +): NotificationCompat.Builder { + this + .addAction( + playPauseIcon, + playPauseText, + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + }, + FLAG_IMMUTABLE + ) + ) + .addAction( + R.drawable.restart, + "Reset", + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.RESET.toString() + }, + FLAG_IMMUTABLE + ) + ) + .addAction( + R.drawable.skip_next, + "Skip", + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.SKIP.toString() + }, + FLAG_IMMUTABLE + ) + ) + + return this +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 976be41..8b32a94 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -7,7 +7,6 @@ import android.media.MediaPlayer import android.os.IBinder import android.os.SystemClock import android.provider.Settings -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.material3.ColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.toArgb @@ -23,6 +22,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.nsh07.pomodoro.R import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.AppContainer import org.nsh07.pomodoro.data.StatRepository @@ -32,7 +32,6 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.utils.millisecondsToStr import kotlin.text.Typography.middleDot -@ExperimentalAnimationApi class TimerService : Service() { private lateinit var appContainer: AppContainer @@ -108,6 +107,13 @@ class TimerService : Service() { } private fun toggleTimer() { + notificationBuilder + .clearActions() + .addTimerActions( + this, + if (timerState.value.timerRunning) R.drawable.pause else R.drawable.play, + if (timerState.value.timerRunning) "Stop" else "Start" + ) if (timerState.value.timerRunning) { showTimerNotification(time.toInt(), paused = true) _timerState.update { currentState -> @@ -326,14 +332,6 @@ class TimerService : Service() { stopSelf() } - private fun setStopButton() { - // TODO - } - - private fun setResumeButton() { - // TODO - } - override fun onDestroy() { super.onDestroy() runBlocking { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 24914b1..b2437f6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -10,7 +10,6 @@ package org.nsh07.pomodoro.ui import android.content.Intent import androidx.compose.animation.ContentTransform import androidx.compose.animation.Crossfade -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleOut @@ -55,10 +54,7 @@ import org.nsh07.pomodoro.ui.timerScreen.TimerScreen import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel -@OptIn( - ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class, - ExperimentalAnimationApi::class -) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppScreen( modifier: Modifier = Modifier, From f28fc8a2c1852d91975440d68aff3653a9967beb Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 15:36:18 +0530 Subject: [PATCH 5/7] feat: Show exact time remaining in Live Update/Now Bar --- app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 8b32a94..b906677 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -139,7 +139,7 @@ class TimerService : Service() { else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt() } - iterations = (iterations + 1) % 50 + iterations = (iterations + 1) % 10 if (iterations == 0) showTimerNotification(time.toInt()) @@ -224,6 +224,7 @@ class TimerService : Service() { ) ) .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time + .setShortCriticalText(millisecondsToStr(time)) .build() ) From c1c2f57ddc86593dd288c3c07b1085da136de1e9 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 16:48:25 +0530 Subject: [PATCH 6/7] fix: Next timer not starting when current timer was completed --- .../org/nsh07/pomodoro/service/AddActions.kt | 86 +++++++++++-------- .../nsh07/pomodoro/service/TimerService.kt | 25 ++++-- .../pomodoro/ui/timerScreen/TimerScreen.kt | 6 +- 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt b/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt index 5475c1e..181e4f9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt @@ -19,44 +19,56 @@ fun NotificationCompat.Builder.addTimerActions( context: Context, @DrawableRes playPauseIcon: Int, playPauseText: String -): NotificationCompat.Builder { - this - .addAction( - playPauseIcon, - playPauseText, - PendingIntent.getService( - context, - 0, - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.TOGGLE.toString() - }, - FLAG_IMMUTABLE - ) +): NotificationCompat.Builder = this + .addAction( + playPauseIcon, + playPauseText, + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + }, + FLAG_IMMUTABLE ) - .addAction( - R.drawable.restart, - "Reset", - PendingIntent.getService( - context, - 0, - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.RESET.toString() - }, - FLAG_IMMUTABLE - ) + ) + .addAction( + R.drawable.restart, + "Reset", + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.RESET.toString() + }, + FLAG_IMMUTABLE ) - .addAction( - R.drawable.skip_next, - "Skip", - PendingIntent.getService( - context, - 0, - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.SKIP.toString() - }, - FLAG_IMMUTABLE - ) + ) + .addAction( + R.drawable.skip_next, + "Skip", + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.SKIP.toString() + }, + FLAG_IMMUTABLE ) + ) - return this -} \ No newline at end of file +fun NotificationCompat.Builder.addStopAlarmAction( + context: Context +): NotificationCompat.Builder = this + .addAction( + R.drawable.alarm, + "Stop alarm", + PendingIntent.getService( + context, + 0, + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.STOP_ALARM.toString() + }, + FLAG_IMMUTABLE + ) + ) \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index b906677..291d2b6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -57,9 +57,9 @@ class TimerService : Service() { private var pauseTime = 0L private var pauseDuration = 0L - private val timerJob = SupervisorJob() - private val scope = CoroutineScope(Dispatchers.IO + timerJob) - private val skipScope = CoroutineScope(Dispatchers.IO) + private var job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) + private val skipScope = CoroutineScope(Dispatchers.IO + job) private lateinit var alarm: MediaPlayer @@ -145,11 +145,10 @@ class TimerService : Service() { if (time < 0) { skipTimer() - _timerState.update { currentState -> currentState.copy(timerRunning = false) } - timerJob.cancel() + break } else { _timerState.update { currentState -> currentState.copy( @@ -168,6 +167,8 @@ class TimerService : Service() { fun showTimerNotification( remainingTime: Int, paused: Boolean = false, complete: Boolean = false ) { + if (complete) notificationBuilder.clearActions().addStopAlarmAction(this) + val totalTime = when (timerState.value.timerMode) { TimerMode.FOCUS -> timerRepository.focusTime.toInt() TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() @@ -224,7 +225,7 @@ class TimerService : Service() { ) ) .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time - .setShortCriticalText(millisecondsToStr(time)) + .setShortCriticalText(millisecondsToStr(time.coerceAtLeast(0))) .build() ) @@ -309,6 +310,16 @@ class TimerService : Service() { _timerState.update { currentState -> currentState.copy(alarmRinging = false) } + notificationBuilder.clearActions().addTimerActions(this, R.drawable.play, "Start") + showTimerNotification( + when (timerState.value.timerMode) { + TimerMode.FOCUS -> timerRepository.focusTime.toInt() + TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() + else -> timerRepository.longBreakTime.toInt() + }, + paused = true, + complete = false + ) } suspend fun saveTimeToDb() { @@ -336,7 +347,7 @@ class TimerService : Service() { override fun onDestroy() { super.onDestroy() runBlocking { - timerJob.cancel() + job.cancel() saveTimeToDb() notificationManager.cancel(1) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt index dae0e4e..ba3a1b2 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt @@ -205,7 +205,7 @@ fun TimerScreen( color = color, trackColor = colorContainer, strokeWidth = 16.dp, - gapSize = 16.dp + gapSize = 8.dp ) } else { CircularWavyProgressIndicator( @@ -229,7 +229,7 @@ fun TimerScreen( cap = StrokeCap.Round, ), wavelength = 60.dp, - gapSize = 16.dp + gapSize = 8.dp ) } var expanded by remember { mutableStateOf(timerState.showBrandTitle) } @@ -300,10 +300,10 @@ fun TimerScreen( { FilledIconToggleButton( onCheckedChange = { checked -> + onAction(TimerAction.ToggleTimer) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } - onAction(TimerAction.ToggleTimer) }, checked = timerState.timerRunning, colors = IconButtonDefaults.filledIconToggleButtonColors( From fdca3cfa2aab5d926cf858d937a9623b41a2b632 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 14 Sep 2025 17:07:32 +0530 Subject: [PATCH 7/7] fix: Show only the progress for the current timer in notification when running on Android 15 or lower --- .../nsh07/pomodoro/service/TimerService.kt | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 291d2b6..82c2ff4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.Service import android.content.Intent import android.media.MediaPlayer +import android.os.Build import android.os.IBinder import android.os.SystemClock import android.provider.Settings @@ -107,20 +108,27 @@ class TimerService : Service() { } private fun toggleTimer() { - notificationBuilder - .clearActions() - .addTimerActions( - this, - if (timerState.value.timerRunning) R.drawable.pause else R.drawable.play, - if (timerState.value.timerRunning) "Stop" else "Start" - ) if (timerState.value.timerRunning) { + notificationBuilder + .clearActions() + .addTimerActions( + this, + R.drawable.play, + "Start" + ) showTimerNotification(time.toInt(), paused = true) _timerState.update { currentState -> currentState.copy(timerRunning = false) } pauseTime = SystemClock.elapsedRealtime() } else { + notificationBuilder + .clearActions() + .addTimerActions( + this, + R.drawable.pause, + "Stop" + ) _timerState.update { it.copy(timerRunning = true) } if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime @@ -202,26 +210,42 @@ class TimerService : Service() { .setStyle( NotificationCompat.ProgressStyle().also { // Add all the Focus, Short break and long break intervals in order - for (i in 0..= Build.VERSION_CODES.BAKLAVA) { + // Android 16 and later supports live updates + // Set progress bar sections if on Baklava or later + for (i in 0.. timerRepository.focusTime.toInt() + TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() + else -> timerRepository.longBreakTime.toInt() + } + ) ) } } .setProgress( // Set the current progress by filling the previous intervals and part of the current interval - (totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + (totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt() + } else (totalTime - remainingTime) ) ) .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time @@ -310,7 +334,7 @@ class TimerService : Service() { _timerState.update { currentState -> currentState.copy(alarmRinging = false) } - notificationBuilder.clearActions().addTimerActions(this, R.drawable.play, "Start") + notificationBuilder.clearActions().addTimerActions(this, R.drawable.play, "Start next") showTimerNotification( when (timerState.value.timerMode) { TimerMode.FOCUS -> timerRepository.focusTime.toInt()