From 5cb864f084d9c1f3dce4e940eab1298325e2d527 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 22:44:45 +0530 Subject: [PATCH] feat(settings): improve auto-reload behaviour --- .../org/nsh07/pomodoro/data/AppContainer.kt | 6 ++ .../nsh07/pomodoro/service/ServiceHelper.kt | 58 +++++++++++++++++++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 30 +--------- .../settingsScreen/screens/TimerSettings.kt | 15 +---- .../viewModel/SettingsViewModel.kt | 39 ++++++++++++- .../timerScreen/viewModel/TimerViewModel.kt | 15 +++-- app/src/main/res/values/strings.xml | 1 + 7 files changed, 115 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.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 3df1ce1..12186a6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import org.nsh07.pomodoro.R import org.nsh07.pomodoro.billing.BillingManager import org.nsh07.pomodoro.billing.BillingManagerProvider +import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.service.addTimerActions import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.utils.millisecondsToStr @@ -41,6 +42,7 @@ interface AppContainer { val notificationManager: NotificationManagerCompat val notificationManagerService: NotificationManager val notificationBuilder: NotificationCompat.Builder + val serviceHelper: ServiceHelper val timerState: MutableStateFlow val time: MutableStateFlow var activityTurnScreenOn: (Boolean) -> Unit @@ -87,6 +89,10 @@ class DefaultAppContainer(context: Context) : AppContainer { .setVisibility(VISIBILITY_PUBLIC) } + override val serviceHelper: ServiceHelper by lazy { + ServiceHelper(context) + } + override val timerState: MutableStateFlow by lazy { MutableStateFlow( TimerState( diff --git a/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt new file mode 100644 index 0000000..bb7bc89 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.service + +import android.content.Context +import android.content.Intent +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction + +/** + * Helper class that holds a reference to [Context] and helps call [Context.startService] in + * [androidx.lifecycle.ViewModel]s. This class must be managed by an [android.app.Application] class + * to scope it to the Activity's lifecycle and prevent leaks. + */ +class ServiceHelper(private val context: Context) { + fun startService(action: TimerAction) { + 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) + } + } + } +} \ 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 e581922..a998da7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -63,7 +63,6 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog 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) @@ -163,34 +162,7 @@ fun AppScreen( timerState = uiState, isPlus = isPlus, progress = { progress }, - 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) - } - } - }, + onAction = timerViewModel::onAction, modifier = modifier .padding( start = contentPadding.calculateStartPadding(layoutDirection), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index 0d28931..da31588 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -62,7 +62,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberSliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -78,7 +77,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider @@ -115,17 +113,6 @@ fun TimerSettings( val notificationManagerService = remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - DisposableEffect(Unit) { - onDispose { - if (!serviceRunning) { - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.RESET.toString() - context.startService(it) - } - } - } - } - val switchItems = listOf( SettingsSwitchItem( checked = settingsState.dndEnabled, @@ -191,7 +178,7 @@ fun TimerSettings( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon(painterResource(R.drawable.info), null) - Text("Reset the timer to change settings") + Text(stringResource(R.string.timer_settings_reset_info)) } } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index a61146f..896eb28 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -43,13 +43,21 @@ import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.billing.BillingManager import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.ui.Screen +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState +import org.nsh07.pomodoro.utils.millisecondsToStr @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class) class SettingsViewModel( private val billingManager: BillingManager, private val preferenceRepository: AppPreferenceRepository, + private val serviceHelper: ServiceHelper, + private val time: MutableStateFlow, private val timerRepository: TimerRepository, + private val timerState: MutableStateFlow ) : ViewModel() { val backStack = mutableStateListOf(Screen.Settings.Main) @@ -107,6 +115,7 @@ class SettingsViewModel( "session_length", sessionsSliderState.value.toInt() ) + refreshTimer() } } @@ -117,6 +126,7 @@ class SettingsViewModel( .collect { if (it.isNotEmpty()) { timerRepository.focusTime = it.toString().toLong() * 60 * 1000 + refreshTimer() preferenceRepository.saveIntPreference( "focus_time", timerRepository.focusTime.toInt() @@ -130,6 +140,7 @@ class SettingsViewModel( .collect { if (it.isNotEmpty()) { timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000 + refreshTimer() preferenceRepository.saveIntPreference( "short_break_time", timerRepository.shortBreakTime.toInt() @@ -143,6 +154,7 @@ class SettingsViewModel( .collect { if (it.isNotEmpty()) { timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000 + refreshTimer() preferenceRepository.saveIntPreference( "long_break_time", timerRepository.longBreakTime.toInt() @@ -153,6 +165,7 @@ class SettingsViewModel( } fun cancelTextFieldFlowCollection() { + if (!serviceRunning.value) serviceHelper.startService(TimerAction.ResetTimer) focusFlowCollectionJob?.cancel() shortBreakFlowCollectionJob?.cancel() longBreakFlowCollectionJob?.cancel() @@ -270,6 +283,24 @@ class SettingsViewModel( } } + private fun refreshTimer() { + if (!serviceRunning.value) { + time.update { timerRepository.focusTime } + + timerState.update { currentState -> + currentState.copy( + timerMode = TimerMode.FOCUS, + timeStr = millisecondsToStr(time.value), + totalTime = time.value, + 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 + ) + } + } + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -277,11 +308,17 @@ class SettingsViewModel( val appBillingManager = application.container.billingManager val appPreferenceRepository = application.container.appPreferenceRepository val appTimerRepository = application.container.appTimerRepository + val serviceHelper = application.container.serviceHelper + val time = application.container.time + val timerState = application.container.timerState SettingsViewModel( billingManager = appBillingManager, preferenceRepository = appPreferenceRepository, - timerRepository = appTimerRepository + serviceHelper = serviceHelper, + time = time, + timerRepository = appTimerRepository, + timerState = timerState ) } } 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 6fefa40..318a862 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 @@ -17,10 +17,9 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel -import android.app.Application import android.provider.Settings import androidx.core.net.toUri -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope @@ -42,19 +41,20 @@ import org.nsh07.pomodoro.data.PreferenceRepository import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.utils.millisecondsToStr import java.time.LocalDate import java.time.temporal.ChronoUnit @OptIn(FlowPreview::class) class TimerViewModel( - application: Application, private val preferenceRepository: PreferenceRepository, + private val serviceHelper: ServiceHelper, private val statRepository: StatRepository, private val timerRepository: TimerRepository, private val _timerState: MutableStateFlow, private val _time: MutableStateFlow -) : AndroidViewModel(application) { +) : ViewModel() { val timerState: StateFlow = _timerState.asStateFlow() val time: StateFlow = _time.asStateFlow() @@ -155,6 +155,10 @@ class TimerViewModel( } } + fun onAction(action: TimerAction) { + serviceHelper.startService(action) + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -162,12 +166,13 @@ class TimerViewModel( val appPreferenceRepository = application.container.appPreferenceRepository val appStatRepository = application.container.appStatRepository val appTimerRepository = application.container.appTimerRepository + val serviceHelper = application.container.serviceHelper val timerState = application.container.timerState val time = application.container.time TimerViewModel( - application = application, preferenceRepository = appPreferenceRepository, + serviceHelper = serviceHelper, statRepository = appStatRepository, timerRepository = appTimerRepository, _timerState = timerState, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 078f5d7..433cd72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,4 +93,5 @@ BuyMeACoffee Selected Help with translation + Reset the timer to change settings \ No newline at end of file