feat(settings): improve auto-reload behaviour

This commit is contained in:
Nishant Mishra
2025-11-09 22:44:45 +05:30
parent 9ec3e6851f
commit 5cb864f084
7 changed files with 115 additions and 49 deletions

View File

@@ -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<TimerState>
val time: MutableStateFlow<Long>
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<TimerState> by lazy {
MutableStateFlow(
TimerState(

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
}
}

View File

@@ -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),

View File

@@ -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))
}
}
}

View File

@@ -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<Long>,
private val timerRepository: TimerRepository,
private val timerState: MutableStateFlow<TimerState>
) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(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
)
}
}

View File

@@ -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<TimerState>,
private val _time: MutableStateFlow<Long>
) : AndroidViewModel(application) {
) : ViewModel() {
val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
val time: StateFlow<Long> = _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,

View File

@@ -93,4 +93,5 @@
<string name="bmc">BuyMeACoffee</string>
<string name="selected">Selected</string>
<string name="help_with_translation">Help with translation</string>
<string name="timer_settings_reset_info">Reset the timer to change settings</string>
</resources>