refactor(architecture): reorganize state variables into a separate state layer

This change also ensures a single source of truth for states used by the UI and those used by the Service. #117
This commit is contained in:
Nishant Mishra
2025-12-03 14:46:27 +05:30
parent 28f0b38290
commit 32a09593f3
10 changed files with 290 additions and 320 deletions

View File

@@ -69,14 +69,14 @@ class MainActivity : ComponentActivity() {
) { ) {
val colorScheme = colorScheme val colorScheme = colorScheme
LaunchedEffect(colorScheme) { LaunchedEffect(colorScheme) {
appContainer.appTimerRepository.colorScheme = colorScheme appContainer.stateRepository.colorScheme = colorScheme
} }
AppScreen( AppScreen(
isPlus = isPlus, isPlus = isPlus,
isAODEnabled = settingsState.aodEnabled, isAODEnabled = settingsState.aodEnabled,
setTimerFrequency = { setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it appContainer.stateRepository.timerFrequency = it
} }
) )
} }
@@ -86,13 +86,13 @@ class MainActivity : ComponentActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// Reduce the timer loop frequency when not visible to save battery power // Reduce the timer loop frequency when not visible to save battery
appContainer.appTimerRepository.timerFrequency = 1f appContainer.stateRepository.timerFrequency = 1f
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Increase the timer loop frequency again when visible to make the progress smoother // Increase the timer loop frequency again when visible to make the progress smoother
appContainer.appTimerRepository.timerFrequency = 60f appContainer.stateRepository.timerFrequency = 60f
} }
} }

View File

@@ -31,19 +31,16 @@ import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.billing.BillingManagerProvider import org.nsh07.pomodoro.billing.BillingManagerProvider
import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.service.addTimerActions import org.nsh07.pomodoro.service.addTimerActions
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
interface AppContainer { interface AppContainer {
val appPreferenceRepository: AppPreferenceRepository val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository val stateRepository: StateRepository
val billingManager: BillingManager val billingManager: BillingManager
val notificationManager: NotificationManagerCompat val notificationManager: NotificationManagerCompat
val notificationManagerService: NotificationManager val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder val notificationBuilder: NotificationCompat.Builder
val serviceHelper: ServiceHelper val serviceHelper: ServiceHelper
val timerState: MutableStateFlow<TimerState>
val time: MutableStateFlow<Long> val time: MutableStateFlow<Long>
var activityTurnScreenOn: (Boolean) -> Unit var activityTurnScreenOn: (Boolean) -> Unit
} }
@@ -58,7 +55,9 @@ class DefaultAppContainer(context: Context) : AppContainer {
AppStatRepository(AppDatabase.getDatabase(context).statDao()) AppStatRepository(AppDatabase.getDatabase(context).statDao())
} }
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() } override val stateRepository: StateRepository by lazy {
StateRepository()
}
override val billingManager: BillingManager by lazy { BillingManagerProvider.manager } override val billingManager: BillingManager by lazy { BillingManagerProvider.manager }
@@ -93,20 +92,9 @@ class DefaultAppContainer(context: Context) : AppContainer {
ServiceHelper(context) ServiceHelper(context)
} }
override val timerState: MutableStateFlow<TimerState> by lazy {
MutableStateFlow(
TimerState(
totalTime = appTimerRepository.focusTime,
timeStr = millisecondsToStr(appTimerRepository.focusTime),
nextTimeStr = millisecondsToStr(appTimerRepository.shortBreakTime)
)
)
}
override val time: MutableStateFlow<Long> by lazy { override val time: MutableStateFlow<Long> by lazy {
MutableStateFlow(appTimerRepository.focusTime) MutableStateFlow(stateRepository.settingsState.value.focusTime)
} }
override var activityTurnScreenOn: (Boolean) -> Unit = {} override var activityTurnScreenOn: (Boolean) -> Unit = {}
} }

View File

@@ -0,0 +1,31 @@
/*
* 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.data
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.lightColorScheme
import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
class StateRepository {
val timerState = MutableStateFlow(TimerState())
val settingsState = MutableStateFlow(SettingsState())
var timerFrequency: Float = 60f
var colorScheme: ColorScheme = lightColorScheme()
}

View File

@@ -1,66 +0,0 @@
/*
* 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.data
import android.net.Uri
import android.provider.Settings
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.lightColorScheme
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Interface that holds the timer durations for each timer type. This repository maintains a single
* source of truth for the timer durations for the various ViewModels in the app.
*/
interface TimerRepository {
var focusTime: Long
var shortBreakTime: Long
var longBreakTime: Long
var sessionLength: Int
var timerFrequency: Float
var alarmEnabled: Boolean
var vibrateEnabled: Boolean
var dndEnabled: Boolean
var colorScheme: ColorScheme
var alarmSoundUri: Uri?
var serviceRunning: MutableStateFlow<Boolean>
}
/**
* See [TimerRepository] for more details
*/
class AppTimerRepository : TimerRepository {
override var focusTime = 25 * 60 * 1000L
override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4
override var timerFrequency: Float = 60f
override var alarmEnabled = true
override var vibrateEnabled = true
override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
override var serviceRunning = MutableStateFlow(false)
}

View File

@@ -36,7 +36,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -53,12 +52,13 @@ class TimerService : Service() {
(application as TomatoApplication).container (application as TomatoApplication).container
} }
private val timerRepository by lazy { appContainer.appTimerRepository } private val stateRepository by lazy { appContainer.stateRepository }
private val statRepository by lazy { appContainer.appStatRepository } private val statRepository by lazy { appContainer.appStatRepository }
private val notificationManager by lazy { appContainer.notificationManager } private val notificationManager by lazy { appContainer.notificationManager }
private val notificationManagerService by lazy { appContainer.notificationManagerService } private val notificationManagerService by lazy { appContainer.notificationManagerService }
private val notificationBuilder by lazy { appContainer.notificationBuilder } private val notificationBuilder by lazy { appContainer.notificationBuilder }
private val _timerState by lazy { appContainer.timerState } private val _timerState by lazy { stateRepository.timerState }
private val _settingsState by lazy { stateRepository.settingsState }
private val _time by lazy { appContainer.time } private val _time by lazy { appContainer.time }
/** /**
@@ -68,8 +68,6 @@ class TimerService : Service() {
get() = _time.value get() = _time.value
set(value) = _time.update { value } set(value) = _time.update { value }
private val timerState by lazy { _timerState.asStateFlow() }
private var cycles = 0 private var cycles = 0
private var startTime = 0L private var startTime = 0L
private var pauseTime = 0L private var pauseTime = 0L
@@ -94,7 +92,7 @@ class TimerService : Service() {
} }
} }
private val cs by lazy { timerRepository.colorScheme } private val cs by lazy { stateRepository.colorScheme }
private lateinit var notificationStyle: NotificationCompat.ProgressStyle private lateinit var notificationStyle: NotificationCompat.ProgressStyle
@@ -104,12 +102,12 @@ class TimerService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
timerRepository.serviceRunning.update { true } stateRepository.timerState.update { it.copy(serviceRunning = true) }
alarm = initializeMediaPlayer() alarm = initializeMediaPlayer()
} }
override fun onDestroy() { override fun onDestroy() {
timerRepository.serviceRunning.update { false } stateRepository.timerState.update { it.copy(serviceRunning = false) }
runBlocking { runBlocking {
job.cancel() job.cancel()
saveTimeToDb() saveTimeToDb()
@@ -129,7 +127,7 @@ class TimerService : Service() {
} }
Actions.RESET.toString() -> { Actions.RESET.toString() -> {
if (timerState.value.timerRunning) toggleTimer() if (_timerState.value.timerRunning) toggleTimer()
skipScope.launch { skipScope.launch {
resetTimer() resetTimer()
stopForegroundService() stopForegroundService()
@@ -148,7 +146,7 @@ class TimerService : Service() {
private fun toggleTimer() { private fun toggleTimer() {
updateProgressSegments() updateProgressSegments()
if (timerState.value.timerRunning) { if (_timerState.value.timerRunning) {
setDoNotDisturb(false) setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.play, getString(R.string.start) this, R.drawable.play, getString(R.string.start)
@@ -159,7 +157,7 @@ class TimerService : Service() {
} }
pauseTime = SystemClock.elapsedRealtime() pauseTime = SystemClock.elapsedRealtime()
} else { } else {
if (timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true) if (_timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true)
else setDoNotDisturb(false) else setDoNotDisturb(false)
notificationBuilder.clearActions().addTimerActions( notificationBuilder.clearActions().addTimerActions(
this, R.drawable.pause, getString(R.string.stop) this, R.drawable.pause, getString(R.string.stop)
@@ -171,19 +169,20 @@ class TimerService : Service() {
timerScope.launch { timerScope.launch {
while (true) { while (true) {
if (!timerState.value.timerRunning) break if (!_timerState.value.timerRunning) break
if (startTime == 0L) startTime = SystemClock.elapsedRealtime() if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
time = when (timerState.value.timerMode) { val settingsState = _settingsState.value
TimerMode.FOCUS -> timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) time = when (_timerState.value.timerMode) {
TimerMode.FOCUS -> settingsState.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration)
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) TimerMode.SHORT_BREAK -> settingsState.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration)
else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) else -> settingsState.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration)
} }
iterations = iterations =
(iterations + 1) % timerRepository.timerFrequency.toInt().coerceAtLeast(1) (iterations + 1) % stateRepository.timerFrequency.toInt().coerceAtLeast(1)
if (iterations == 0) showTimerNotification(time.toInt()) if (iterations == 0) showTimerNotification(time.toInt())
@@ -199,7 +198,7 @@ class TimerService : Service() {
timeStr = millisecondsToStr(time) timeStr = millisecondsToStr(time)
) )
} }
val totalTime = timerState.value.totalTime val totalTime = _timerState.value.totalTime
if (totalTime - time < lastSavedDuration) if (totalTime - time < lastSavedDuration)
lastSavedDuration = lastSavedDuration =
@@ -208,7 +207,7 @@ class TimerService : Service() {
saveTimeToDb() saveTimeToDb()
} }
delay((1000f / timerRepository.timerFrequency).toLong()) delay((1000f / stateRepository.timerFrequency).toLong())
} }
} }
} }
@@ -221,21 +220,23 @@ class TimerService : Service() {
fun showTimerNotification( fun showTimerNotification(
remainingTime: Int, paused: Boolean = false, complete: Boolean = false remainingTime: Int, paused: Boolean = false, complete: Boolean = false
) { ) {
val settingsState = _settingsState.value
if (complete) notificationBuilder.clearActions().addStopAlarmAction(this) if (complete) notificationBuilder.clearActions().addStopAlarmAction(this)
val totalTime = when (timerState.value.timerMode) { val totalTime = when (_timerState.value.timerMode) {
TimerMode.FOCUS -> timerRepository.focusTime.toInt() TimerMode.FOCUS -> settingsState.focusTime.toInt()
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt()
else -> timerRepository.longBreakTime.toInt() else -> settingsState.longBreakTime.toInt()
} }
val currentTimer = when (timerState.value.timerMode) { val currentTimer = when (_timerState.value.timerMode) {
TimerMode.FOCUS -> getString(R.string.focus) TimerMode.FOCUS -> getString(R.string.focus)
TimerMode.SHORT_BREAK -> getString(R.string.short_break) TimerMode.SHORT_BREAK -> getString(R.string.short_break)
else -> getString(R.string.long_break) else -> getString(R.string.long_break)
} }
val nextTimer = when (timerState.value.nextTimerMode) { val nextTimer = when (_timerState.value.nextTimerMode) {
TimerMode.FOCUS -> getString(R.string.focus) TimerMode.FOCUS -> getString(R.string.focus)
TimerMode.SHORT_BREAK -> getString(R.string.short_break) TimerMode.SHORT_BREAK -> getString(R.string.short_break)
else -> getString(R.string.long_break) else -> getString(R.string.long_break)
@@ -258,14 +259,14 @@ class TimerService : Service() {
getString( getString(
R.string.up_next_notification, R.string.up_next_notification,
nextTimer, nextTimer,
timerState.value.nextTimeStr _timerState.value.nextTimeStr
) )
) )
.setStyle( .setStyle(
notificationStyle notificationStyle
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval .setProgress( // Set the current progress by filling the previous intervals and part of the current interval
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
(totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt() (totalTime - remainingTime) + ((cycles + 1) / 2) * settingsState.focusTime.toInt() + (cycles / 2) * settingsState.shortBreakTime.toInt()
} else (totalTime - remainingTime) } else (totalTime - remainingTime)
) )
) )
@@ -283,37 +284,38 @@ class TimerService : Service() {
} }
private fun updateProgressSegments() { private fun updateProgressSegments() {
val settingsState = _settingsState.value
notificationStyle = NotificationCompat.ProgressStyle() notificationStyle = NotificationCompat.ProgressStyle()
.also { .also {
// Add all the Focus, Short break and long break intervals in order // Add all the Focus, Short break and long break intervals in order
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
// Android 16 and later supports live updates // Android 16 and later supports live updates
// Set progress bar sections if on Baklava or later // Set progress bar sections if on Baklava or later
for (i in 0..<timerRepository.sessionLength * 2) { for (i in 0..<settingsState.sessionLength * 2) {
if (i % 2 == 0) it.addProgressSegment( if (i % 2 == 0) it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment( NotificationCompat.ProgressStyle.Segment(
timerRepository.focusTime.toInt() settingsState.focusTime.toInt()
) )
.setColor(cs.primary.toArgb()) .setColor(cs.primary.toArgb())
) )
else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment( else if (i != (settingsState.sessionLength * 2 - 1)) it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment( NotificationCompat.ProgressStyle.Segment(
timerRepository.shortBreakTime.toInt() settingsState.shortBreakTime.toInt()
).setColor(cs.tertiary.toArgb()) ).setColor(cs.tertiary.toArgb())
) )
else it.addProgressSegment( else it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment( NotificationCompat.ProgressStyle.Segment(
timerRepository.longBreakTime.toInt() settingsState.longBreakTime.toInt()
).setColor(cs.tertiary.toArgb()) ).setColor(cs.tertiary.toArgb())
) )
} }
} else { } else {
it.addProgressSegment( it.addProgressSegment(
NotificationCompat.ProgressStyle.Segment( NotificationCompat.ProgressStyle.Segment(
when (timerState.value.timerMode) { when (_timerState.value.timerMode) {
TimerMode.FOCUS -> timerRepository.focusTime.toInt() TimerMode.FOCUS -> settingsState.focusTime.toInt()
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt()
else -> timerRepository.longBreakTime.toInt() else -> settingsState.longBreakTime.toInt()
} }
) )
) )
@@ -322,10 +324,12 @@ class TimerService : Service() {
} }
private suspend fun resetTimer() { private suspend fun resetTimer() {
val settingsState = _settingsState.value
updateProgressSegments() updateProgressSegments()
saveTimeToDb() saveTimeToDb()
lastSavedDuration = 0 lastSavedDuration = 0
time = timerRepository.focusTime time = settingsState.focusTime
cycles = 0 cycles = 0
startTime = 0L startTime = 0L
pauseTime = 0L pauseTime = 0L
@@ -336,15 +340,16 @@ class TimerService : Service() {
timerMode = TimerMode.FOCUS, timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time), timeStr = millisecondsToStr(time),
totalTime = time, totalTime = time,
nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime),
currentFocusCount = 1, currentFocusCount = 1,
totalFocusCount = timerRepository.sessionLength totalFocusCount = settingsState.sessionLength
) )
} }
} }
private suspend fun skipTimer(fromButton: Boolean = false) { private suspend fun skipTimer(fromButton: Boolean = false) {
val settingsState = _settingsState.value
updateProgressSegments() updateProgressSegments()
saveTimeToDb() saveTimeToDb()
updateProgressSegments() updateProgressSegments()
@@ -354,30 +359,30 @@ class TimerService : Service() {
pauseTime = 0L pauseTime = 0L
pauseDuration = 0L pauseDuration = 0L
cycles = (cycles + 1) % (timerRepository.sessionLength * 2) cycles = (cycles + 1) % (settingsState.sessionLength * 2)
if (cycles % 2 == 0) { if (cycles % 2 == 0) {
if (timerState.value.timerRunning) setDoNotDisturb(true) if (_timerState.value.timerRunning) setDoNotDisturb(true)
time = timerRepository.focusTime time = settingsState.focusTime
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy( currentState.copy(
timerMode = TimerMode.FOCUS, timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time), timeStr = millisecondsToStr(time),
totalTime = time, totalTime = time,
nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, nextTimerMode = if (cycles == (settingsState.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( nextTimeStr = if (cycles == (settingsState.sessionLength - 1) * 2) millisecondsToStr(
timerRepository.longBreakTime settingsState.longBreakTime
) else millisecondsToStr( ) else millisecondsToStr(
timerRepository.shortBreakTime settingsState.shortBreakTime
), ),
currentFocusCount = cycles / 2 + 1, currentFocusCount = cycles / 2 + 1,
totalFocusCount = timerRepository.sessionLength totalFocusCount = settingsState.sessionLength
) )
} }
} else { } else {
if (timerState.value.timerRunning) setDoNotDisturb(false) if (_timerState.value.timerRunning) setDoNotDisturb(false)
val long = cycles == (timerRepository.sessionLength * 2) - 1 val long = cycles == (settingsState.sessionLength * 2) - 1
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime time = if (long) settingsState.longBreakTime else settingsState.shortBreakTime
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy( currentState.copy(
@@ -385,14 +390,15 @@ class TimerService : Service() {
timeStr = millisecondsToStr(time), timeStr = millisecondsToStr(time),
totalTime = time, totalTime = time,
nextTimerMode = TimerMode.FOCUS, nextTimerMode = TimerMode.FOCUS,
nextTimeStr = millisecondsToStr(timerRepository.focusTime) nextTimeStr = millisecondsToStr(settingsState.focusTime)
) )
} }
} }
} }
fun startAlarm() { fun startAlarm() {
if (timerRepository.alarmEnabled) alarm?.start() val settingsState = _settingsState.value
if (settingsState.alarmEnabled) alarm?.start()
appContainer.activityTurnScreenOn(true) appContainer.activityTurnScreenOn(true)
@@ -401,7 +407,7 @@ class TimerService : Service() {
stopAlarm() stopAlarm()
} }
if (timerRepository.vibrateEnabled) { if (settingsState.vibrateEnabled) {
if (!vibrator.hasVibrator()) { if (!vibrator.hasVibrator()) {
return return
} }
@@ -413,14 +419,15 @@ class TimerService : Service() {
} }
fun stopAlarm() { fun stopAlarm() {
val settingsState = _settingsState.value
autoAlarmStopScope?.cancel() autoAlarmStopScope?.cancel()
if (timerRepository.alarmEnabled) { if (settingsState.alarmEnabled) {
alarm?.pause() alarm?.pause()
alarm?.seekTo(0) alarm?.seekTo(0)
} }
if (timerRepository.vibrateEnabled) { if (settingsState.vibrateEnabled) {
vibrator.cancel() vibrator.cancel()
} }
@@ -434,10 +441,10 @@ class TimerService : Service() {
getString(R.string.start_next) getString(R.string.start_next)
) )
showTimerNotification( showTimerNotification(
when (timerState.value.timerMode) { when (_timerState.value.timerMode) {
TimerMode.FOCUS -> timerRepository.focusTime.toInt() TimerMode.FOCUS -> settingsState.focusTime.toInt()
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt()
else -> timerRepository.longBreakTime.toInt() else -> settingsState.longBreakTime.toInt()
}, paused = true, complete = false }, paused = true, complete = false
) )
} }
@@ -451,7 +458,7 @@ class TimerService : Service() {
.setUsage(AudioAttributes.USAGE_ALARM) .setUsage(AudioAttributes.USAGE_ALARM)
.build() .build()
) )
timerRepository.alarmSoundUri?.let { _settingsState.value.alarmSoundUri?.let {
setDataSource(applicationContext, it) setDataSource(applicationContext, it)
prepare() prepare()
} }
@@ -463,7 +470,7 @@ class TimerService : Service() {
} }
private fun setDoNotDisturb(doNotDisturb: Boolean) { private fun setDoNotDisturb(doNotDisturb: Boolean) {
if (timerRepository.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) { if (_settingsState.value.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) {
if (doNotDisturb) { if (doNotDisturb) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS) notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS)
} else notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) } else notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
@@ -477,8 +484,8 @@ class TimerService : Service() {
suspend fun saveTimeToDb() { suspend fun saveTimeToDb() {
saveLock.withLock { saveLock.withLock {
val elapsedTime = timerState.value.totalTime - time val elapsedTime = _timerState.value.totalTime - time
when (timerState.value.timerMode) { when (_timerState.value.timerMode) {
TimerMode.FOCUS -> statRepository.addFocusTime( TimerMode.FOCUS -> statRepository.addFocusTime(
(elapsedTime - lastSavedDuration).coerceAtLeast(0L) (elapsedTime - lastSavedDuration).coerceAtLeast(0L)
) )

View File

@@ -62,7 +62,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
@@ -92,10 +91,10 @@ fun AlarmSettings(
var alarmName by remember { mutableStateOf("...") } var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(settingsState.alarmSound) { LaunchedEffect(settingsState.alarmSoundUri) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
alarmName = alarmName =
RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri()) RingtoneManager.getRingtone(context, settingsState.alarmSoundUri)
?.getTitle(context) ?: "" ?.getTitle(context) ?: ""
} }
} }
@@ -119,11 +118,11 @@ fun AlarmSettings(
} }
@SuppressLint("LocalContextGetResourceValueCall") @SuppressLint("LocalContextGetResourceValueCall")
val ringtonePickerIntent = remember(settingsState.alarmSound) { val ringtonePickerIntent = remember(settingsState.alarmSoundUri) {
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound)) putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri()) putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSoundUri)
} }
} }

View File

@@ -17,17 +17,27 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri
import android.provider.Settings
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@Immutable @Immutable
data class SettingsState( data class SettingsState(
val theme: String = "auto", val theme: String = "auto",
val alarmSound: String = "",
val colorScheme: String = Color.White.toString(), val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false, val blackTheme: Boolean = false,
val aodEnabled: Boolean = false, val aodEnabled: Boolean = false,
val alarmEnabled: Boolean = true, val alarmEnabled: Boolean = true,
val vibrateEnabled: Boolean = true, val vibrateEnabled: Boolean = true,
val dndEnabled: Boolean = false val dndEnabled: Boolean = false,
val focusTime: Long = 25 * 60 * 1000L,
val shortBreakTime: Long = 5 * 60 * 1000L,
val longBreakTime: Long = 15 * 60 * 1000L,
val sessionLength: Int = 4,
val alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
) )

View File

@@ -25,6 +25,7 @@ import androidx.compose.material3.SliderState
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
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
@@ -35,51 +36,57 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.billing.BillingManager import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.StateRepository
import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr import org.nsh07.pomodoro.utils.millisecondsToStr
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class) @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel( class SettingsViewModel(
private val billingManager: BillingManager, private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository, private val preferenceRepository: PreferenceRepository,
private val stateRepository: StateRepository,
private val serviceHelper: ServiceHelper, private val serviceHelper: ServiceHelper,
private val time: MutableStateFlow<Long>, private val time: MutableStateFlow<Long>
private val timerRepository: TimerRepository,
private val timerState: MutableStateFlow<TimerState>
) : ViewModel() { ) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main) val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus val isPlus = billingManager.isPlus
val serviceRunning = timerRepository.serviceRunning.asStateFlow() val serviceRunning = stateRepository.timerState.map { it.serviceRunning }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
false
)
private val _settingsState = MutableStateFlow(SettingsState()) private val _settingsState = stateRepository.settingsState
val settingsState = _settingsState.asStateFlow() val settingsState = _settingsState.asStateFlow()
val focusTimeTextFieldState by lazy { val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString()) TextFieldState((_settingsState.value.focusTime / 60000).toString())
} }
val shortBreakTimeTextFieldState by lazy { val shortBreakTimeTextFieldState by lazy {
TextFieldState((timerRepository.shortBreakTime / 60000).toString()) TextFieldState((_settingsState.value.shortBreakTime / 60000).toString())
} }
val longBreakTimeTextFieldState by lazy { val longBreakTimeTextFieldState by lazy {
TextFieldState((timerRepository.longBreakTime / 60000).toString()) TextFieldState((_settingsState.value.longBreakTime / 60000).toString())
} }
val sessionsSliderState by lazy { val sessionsSliderState by lazy {
SliderState( SliderState(
value = timerRepository.sessionLength.toFloat(), value = _settingsState.value.sessionLength.toFloat(),
steps = 4, steps = 4,
valueRange = 1f..6f, valueRange = 1f..6f,
onValueChangeFinished = ::updateSessionLength onValueChangeFinished = ::updateSessionLength
@@ -110,11 +117,15 @@ class SettingsViewModel(
} }
private fun updateSessionLength() { private fun updateSessionLength() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
timerRepository.sessionLength = preferenceRepository.saveIntPreference( _settingsState.update { currentState ->
"session_length", currentState.copy(
sessionsSliderState.value.toInt() sessionLength = preferenceRepository.saveIntPreference(
) "session_length",
sessionsSliderState.value.toInt()
)
)
}
refreshTimer() refreshTimer()
} }
} }
@@ -125,11 +136,13 @@ class SettingsViewModel(
.debounce(500) .debounce(500)
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.focusTime = it.toString().toLong() * 60 * 1000 _settingsState.update { currentState ->
currentState.copy(focusTime = it.toString().toLong() * 60 * 1000)
}
refreshTimer() refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"focus_time", "focus_time",
timerRepository.focusTime.toInt() _settingsState.value.focusTime.toInt()
) )
} }
} }
@@ -139,11 +152,13 @@ class SettingsViewModel(
.debounce(500) .debounce(500)
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000 _settingsState.update { currentState ->
currentState.copy(shortBreakTime = it.toString().toLong() * 60 * 1000)
}
refreshTimer() refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"short_break_time", "short_break_time",
timerRepository.shortBreakTime.toInt() _settingsState.value.shortBreakTime.toInt()
) )
} }
} }
@@ -153,11 +168,13 @@ class SettingsViewModel(
.debounce(500) .debounce(500)
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000 _settingsState.update { currentState ->
currentState.copy(longBreakTime = it.toString().toLong() * 60 * 1000)
}
refreshTimer() refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"long_break_time", "long_break_time",
timerRepository.longBreakTime.toInt() _settingsState.value.longBreakTime.toInt()
) )
} }
} }
@@ -173,7 +190,6 @@ class SettingsViewModel(
private fun saveAlarmEnabled(enabled: Boolean) { private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmEnabled = enabled
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(alarmEnabled = enabled) currentState.copy(alarmEnabled = enabled)
} }
@@ -183,7 +199,6 @@ class SettingsViewModel(
private fun saveVibrateEnabled(enabled: Boolean) { private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.vibrateEnabled = enabled
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(vibrateEnabled = enabled) currentState.copy(vibrateEnabled = enabled)
} }
@@ -193,7 +208,6 @@ class SettingsViewModel(
private fun saveDndEnabled(enabled: Boolean) { private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.dndEnabled = enabled
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(dndEnabled = enabled) currentState.copy(dndEnabled = enabled)
} }
@@ -203,9 +217,8 @@ class SettingsViewModel(
private fun saveAlarmSound(uri: Uri?) { private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(alarmSound = uri.toString()) currentState.copy(alarmSoundUri = uri)
} }
preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
} }
@@ -248,32 +261,71 @@ class SettingsViewModel(
} }
suspend fun reloadSettings() { suspend fun reloadSettings() {
var settingsState = _settingsState.value
val focusTime =
preferenceRepository.getIntPreference("focus_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"focus_time",
settingsState.focusTime.toInt()
).toLong()
val shortBreakTime =
preferenceRepository.getIntPreference("short_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"short_break_time",
settingsState.shortBreakTime.toInt()
).toLong()
val longBreakTime =
preferenceRepository.getIntPreference("long_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"long_break_time",
settingsState.longBreakTime.toInt()
).toLong()
val sessionLength =
preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference(
"session_length",
settingsState.sessionLength
)
val alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
).toUri()
val theme = preferenceRepository.getStringPreference("theme") val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto") ?: preferenceRepository.saveStringPreference("theme", settingsState.theme)
val colorScheme = preferenceRepository.getStringPreference("color_scheme") val colorScheme = preferenceRepository.getStringPreference("color_scheme")
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString()) ?: preferenceRepository.saveStringPreference("color_scheme", settingsState.colorScheme)
val blackTheme = preferenceRepository.getBooleanPreference("black_theme") val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
?: preferenceRepository.saveBooleanPreference("black_theme", false) ?: preferenceRepository.saveBooleanPreference("black_theme", settingsState.blackTheme)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false) ?: preferenceRepository.saveBooleanPreference("aod_enabled", settingsState.aodEnabled)
val alarmSound = preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled") val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true) ?: preferenceRepository.saveBooleanPreference(
"alarm_enabled",
settingsState.alarmEnabled
)
val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled") val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true) ?: preferenceRepository.saveBooleanPreference(
"vibrate_enabled",
settingsState.vibrateEnabled
)
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled") val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false) ?: preferenceRepository.saveBooleanPreference("dnd_enabled", settingsState.dndEnabled)
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy( currentState.copy(
focusTime = focusTime,
shortBreakTime = shortBreakTime,
longBreakTime = longBreakTime,
sessionLength = sessionLength,
theme = theme, theme = theme,
colorScheme = colorScheme, colorScheme = colorScheme,
alarmSound = alarmSound, alarmSoundUri = alarmSoundUri,
blackTheme = blackTheme, blackTheme = blackTheme,
aodEnabled = aodEnabled, aodEnabled = aodEnabled,
alarmEnabled = alarmEnabled, alarmEnabled = alarmEnabled,
@@ -281,21 +333,40 @@ class SettingsViewModel(
dndEnabled = dndEnabled dndEnabled = dndEnabled
) )
} }
}
private fun refreshTimer() { settingsState = _settingsState.value
if (!serviceRunning.value) {
time.update { timerRepository.focusTime }
timerState.update { currentState -> time.update { settingsState.focusTime }
if (!stateRepository.timerState.value.serviceRunning)
stateRepository.timerState.update { currentState ->
currentState.copy( currentState.copy(
timerMode = TimerMode.FOCUS, timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value), timeStr = millisecondsToStr(time.value),
totalTime = time.value, totalTime = time.value,
nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime),
currentFocusCount = 1, currentFocusCount = 1,
totalFocusCount = timerRepository.sessionLength totalFocusCount = settingsState.sessionLength
)
}
}
private fun refreshTimer() {
if (!serviceRunning.value) {
val settingsState = _settingsState.value
time.update { settingsState.focusTime }
stateRepository.timerState.update { currentState ->
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
totalTime = time.value,
nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime),
currentFocusCount = 1,
totalFocusCount = settingsState.sessionLength
) )
} }
} }
@@ -307,18 +378,16 @@ class SettingsViewModel(
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appBillingManager = application.container.billingManager val appBillingManager = application.container.billingManager
val appPreferenceRepository = application.container.appPreferenceRepository val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository
val serviceHelper = application.container.serviceHelper val serviceHelper = application.container.serviceHelper
val stateRepository = application.container.stateRepository
val time = application.container.time val time = application.container.time
val timerState = application.container.timerState
SettingsViewModel( SettingsViewModel(
billingManager = appBillingManager, billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
serviceHelper = serviceHelper, serviceHelper = serviceHelper,
time = time, stateRepository = stateRepository,
timerRepository = appTimerRepository, time = time
timerState = timerState
) )
} }
} }

View File

@@ -1,8 +1,18 @@
/* /*
* Copyright (c) 2025 Nishant Mishra * Copyright (c) 2025 Nishant Mishra
* *
* You should have received a copy of the GNU General Public License * This file is part of Tomato - a minimalist pomodoro timer for Android.
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* 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.ui.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
@@ -17,7 +27,8 @@ data class TimerState(
val showBrandTitle: Boolean = true, val showBrandTitle: Boolean = true,
val currentFocusCount: Int = 1, val currentFocusCount: Int = 1,
val totalFocusCount: Int = 4, val totalFocusCount: Int = 4,
val alarmRinging: Boolean = false val alarmRinging: Boolean = false,
val serviceRunning: Boolean = false
) )
enum class TimerMode { enum class TimerMode {

View File

@@ -17,8 +17,6 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.provider.Settings
import androidx.core.net.toUri
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
@@ -37,122 +35,49 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.StateRepository
import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate import java.time.LocalDate
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class TimerViewModel( class TimerViewModel(
private val preferenceRepository: PreferenceRepository,
private val serviceHelper: ServiceHelper, private val serviceHelper: ServiceHelper,
private val stateRepository: StateRepository,
private val statRepository: StatRepository, private val statRepository: StatRepository,
private val timerRepository: TimerRepository,
private val _timerState: MutableStateFlow<TimerState>,
private val _time: MutableStateFlow<Long> private val _time: MutableStateFlow<Long>
) : ViewModel() { ) : ViewModel() {
val timerState: StateFlow<TimerState> = _timerState.asStateFlow() val timerState: StateFlow<TimerState> = stateRepository.timerState.asStateFlow()
val time: StateFlow<Long> = _time.asStateFlow() val time: StateFlow<Long> = _time.asStateFlow()
val progress = _time.combine(_timerState) { remainingTime, uiState -> val progress = _time.combine(stateRepository.timerState) { remainingTime, uiState ->
(uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime (uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f)
private var cycles = 0
private var startTime = 0L
private var pauseTime = 0L
private var pauseDuration = 0L
init { init {
if (!timerRepository.serviceRunning.value) viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) { var lastDate = statRepository.getLastDate()
timerRepository.focusTime = val today = LocalDate.now()
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 = // Fills dates between today and lastDate with 0s to ensure continuous history
preferenceRepository.getBooleanPreference("alarm_enabled") if (lastDate != null) {
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true) while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
timerRepository.vibrateEnabled = lastDate = lastDate?.plusDays(1)
preferenceRepository.getBooleanPreference("vibrate_enabled") statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
timerRepository.dndEnabled =
preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
).toUri()
_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
)
}
var lastDate = statRepository.getLastDate()
val today = LocalDate.now()
// 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))
}
} else {
statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0))
}
delay(1500)
_timerState.update { currentState ->
currentState.copy(showBrandTitle = false)
} }
} else {
statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0))
} }
delay(1500)
stateRepository.timerState.update { currentState ->
currentState.copy(showBrandTitle = false)
}
}
} }
fun onAction(action: TimerAction) { fun onAction(action: TimerAction) {
@@ -163,19 +88,15 @@ class TimerViewModel(
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferenceRepository
val appStatRepository = application.container.appStatRepository val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository val stateRepository = application.container.stateRepository
val serviceHelper = application.container.serviceHelper val serviceHelper = application.container.serviceHelper
val timerState = application.container.timerState
val time = application.container.time val time = application.container.time
TimerViewModel( TimerViewModel(
preferenceRepository = appPreferenceRepository,
serviceHelper = serviceHelper, serviceHelper = serviceHelper,
stateRepository = stateRepository,
statRepository = appStatRepository, statRepository = appStatRepository,
timerRepository = appTimerRepository,
_timerState = timerState,
_time = time _time = time
) )
} }