fix: Fix a bug that caused the app state to reset

When closing the app with the timer still running
This commit is contained in:
Nishant Mishra
2025-09-27 17:41:45 +05:30
parent 2286760ba5
commit 93a6860f93
5 changed files with 117 additions and 108 deletions

View File

@@ -31,6 +31,8 @@ interface TimerRepository {
var colorScheme: ColorScheme
var alarmSoundUri: Uri?
var serviceRunning: Boolean
}
/**
@@ -47,4 +49,5 @@ class AppTimerRepository : TimerRepository {
override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
override var serviceRunning = false
}

View File

@@ -77,9 +77,21 @@ class TimerService : Service() {
override fun onCreate() {
super.onCreate()
timerRepository.serviceRunning = true
alarm = MediaPlayer.create(this, timerRepository.alarmSoundUri)
}
override fun onDestroy() {
super.onDestroy()
timerRepository.serviceRunning = false
runBlocking {
job.cancel()
saveTimeToDb()
notificationManager.cancel(1)
alarm?.release()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
Actions.TOGGLE.toString() -> {
@@ -283,6 +295,7 @@ class TimerService : Service() {
private fun skipTimer(fromButton: Boolean = false) {
skipScope.launch {
saveTimeToDb()
updateProgressSegments()
showTimerNotification(0, paused = true, complete = !fromButton)
startTime = 0L
pauseTime = 0L
@@ -388,16 +401,6 @@ class TimerService : Service() {
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
runBlocking {
job.cancel()
saveTimeToDb()
notificationManager.cancel(1)
alarm?.release()
}
}
enum class Actions {
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
}

View File

@@ -53,6 +53,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -95,6 +96,12 @@ fun SettingsScreenRoot(
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
val context = LocalContext.current
DisposableEffect(Unit) {
viewModel.runTextFieldFlowCollection()
onDispose { viewModel.cancelTextFieldFlowCollection() }
}
val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) {
viewModel.focusTimeTextFieldState
}

View File

@@ -21,6 +21,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
@@ -39,12 +40,15 @@ class SettingsViewModel(
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
val focusTimeTextFieldState =
val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString())
val shortBreakTimeTextFieldState =
}
val shortBreakTimeTextFieldState by lazy {
TextFieldState((timerRepository.shortBreakTime / 60000).toString())
val longBreakTimeTextFieldState =
}
val longBreakTimeTextFieldState by lazy {
TextFieldState((timerRepository.longBreakTime / 60000).toString())
}
val sessionsSliderState = SliderState(
value = timerRepository.sessionLength.toFloat(),
@@ -55,6 +59,8 @@ class SettingsViewModel(
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
private val flowCollectionJob = SupervisorJob()
val alarmSound =
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
val alarmEnabled =
@@ -79,7 +85,19 @@ class SettingsViewModel(
)
}
}
viewModelScope.launch(Dispatchers.IO) {
}
private fun updateSessionLength() {
viewModelScope.launch {
timerRepository.sessionLength = preferenceRepository.saveIntPreference(
"session_length",
sessionsSliderState.value.toInt()
)
}
}
fun runTextFieldFlowCollection() {
viewModelScope.launch(flowCollectionJob + Dispatchers.IO) {
snapshotFlow { focusTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -91,7 +109,7 @@ class SettingsViewModel(
}
}
}
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(flowCollectionJob + Dispatchers.IO) {
snapshotFlow { shortBreakTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -103,7 +121,7 @@ class SettingsViewModel(
}
}
}
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(flowCollectionJob + Dispatchers.IO) {
snapshotFlow { longBreakTimeTextFieldState.text }
.debounce(500)
.collect {
@@ -117,14 +135,7 @@ class SettingsViewModel(
}
}
private fun updateSessionLength() {
viewModelScope.launch {
timerRepository.sessionLength = preferenceRepository.saveIntPreference(
"session_length",
sessionsSliderState.value.toInt()
)
}
}
fun cancelTextFieldFlowCollection() = flowCollectionJob.cancel()
fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch {

View File

@@ -52,98 +52,83 @@ class TimerViewModel(
private var pauseDuration = 0L
init {
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime =
preferenceRepository.getIntPreference("focus_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"focus_time",
timerRepository.focusTime.toInt()
).toLong()
timerRepository.shortBreakTime =
preferenceRepository.getIntPreference("short_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"short_break_time",
timerRepository.shortBreakTime.toInt()
).toLong()
timerRepository.longBreakTime =
preferenceRepository.getIntPreference("long_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"long_break_time",
timerRepository.longBreakTime.toInt()
).toLong()
timerRepository.sessionLength = preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference(
"session_length",
timerRepository.sessionLength
)
timerRepository.alarmEnabled =
preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
timerRepository.vibrateEnabled =
preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
if (!timerRepository.serviceRunning)
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime =
preferenceRepository.getIntPreference("focus_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"focus_time",
timerRepository.focusTime.toInt()
).toLong()
timerRepository.shortBreakTime =
preferenceRepository.getIntPreference("short_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"short_break_time",
timerRepository.shortBreakTime.toInt()
).toLong()
timerRepository.longBreakTime =
preferenceRepository.getIntPreference("long_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"long_break_time",
timerRepository.longBreakTime.toInt()
).toLong()
timerRepository.sessionLength =
preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference(
"session_length",
timerRepository.sessionLength
)
).toUri()
resetTimer()
timerRepository.alarmEnabled =
preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
timerRepository.vibrateEnabled =
preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
var lastDate = statRepository.getLastDate()
val today = LocalDate.now()
timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
).toUri()
// Fills dates between today and lastDate with 0s to ensure continuous history
if (lastDate != null)
while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
lastDate = lastDate?.plusDays(1)
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
pauseTime = 0L
pauseDuration = 0L
_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
)
}
delay(1500)
var lastDate = statRepository.getLastDate()
val today = LocalDate.now()
_timerState.update { currentState ->
currentState.copy(showBrandTitle = false)
// Fills dates between today and lastDate with 0s to ensure continuous history
if (lastDate != null)
while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
lastDate = lastDate?.plusDays(1)
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
}
delay(1500)
_timerState.update { currentState ->
currentState.copy(showBrandTitle = false)
}
}
}
}
private fun resetTimer() {
viewModelScope.launch {
saveTimeToDb()
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
pauseTime = 0L
pauseDuration = 0L
_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
)
}
}
}
suspend fun saveTimeToDb() {
when (timerState.value.timerMode) {
TimerMode.FOCUS -> statRepository
.addFocusTime((timerState.value.totalTime - time.value).coerceAtLeast(0L))
else -> statRepository
.addBreakTime((timerState.value.totalTime - time.value).coerceAtLeast(0L))
}
}
companion object {