feat(settings): implement a settings option to enable auto-dnd

This commit is contained in:
Nishant Mishra
2025-10-25 14:27:25 +05:30
parent d3cead00ab
commit fddc1f8ed3
8 changed files with 171 additions and 56 deletions

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.data package org.nsh07.pomodoro.data
@@ -27,6 +37,7 @@ interface TimerRepository {
var alarmEnabled: Boolean var alarmEnabled: Boolean
var vibrateEnabled: Boolean var vibrateEnabled: Boolean
var dndEnabled: Boolean
var colorScheme: ColorScheme var colorScheme: ColorScheme
@@ -46,6 +57,7 @@ class AppTimerRepository : TimerRepository {
override var timerFrequency: Float = 10f override var timerFrequency: Float = 10f
override var alarmEnabled = true override var alarmEnabled = true
override var vibrateEnabled = true override var vibrateEnabled = true
override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme() override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? = override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI

View File

@@ -129,7 +129,7 @@ class TimerService : Service() {
} }
} }
Actions.SKIP.toString() -> skipTimer(true) Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) }
Actions.STOP_ALARM.toString() -> stopAlarm() Actions.STOP_ALARM.toString() -> stopAlarm()
@@ -329,50 +329,48 @@ class TimerService : Service() {
} }
} }
private fun skipTimer(fromButton: Boolean = false) { private suspend fun skipTimer(fromButton: Boolean = false) {
updateProgressSegments() updateProgressSegments()
skipScope.launch { saveTimeToDb()
saveTimeToDb() updateProgressSegments()
updateProgressSegments() showTimerNotification(0, paused = true, complete = !fromButton)
showTimerNotification(0, paused = true, complete = !fromButton) startTime = 0L
startTime = 0L pauseTime = 0L
pauseTime = 0L pauseDuration = 0L
pauseDuration = 0L
cycles = (cycles + 1) % (timerRepository.sessionLength * 2) cycles = (cycles + 1) % (timerRepository.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 = timerRepository.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 == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
timerRepository.longBreakTime timerRepository.longBreakTime
) else millisecondsToStr( ) else millisecondsToStr(
timerRepository.shortBreakTime timerRepository.shortBreakTime
), ),
currentFocusCount = cycles / 2 + 1, currentFocusCount = cycles / 2 + 1,
totalFocusCount = timerRepository.sessionLength totalFocusCount = timerRepository.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 == (timerRepository.sessionLength * 2) - 1
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy( currentState.copy(
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
timeStr = millisecondsToStr(time), timeStr = millisecondsToStr(time),
totalTime = time, totalTime = time,
nextTimerMode = TimerMode.FOCUS, nextTimerMode = TimerMode.FOCUS,
nextTimeStr = millisecondsToStr(timerRepository.focusTime) nextTimeStr = millisecondsToStr(timerRepository.focusTime)
) )
}
} }
} }
} }
@@ -449,7 +447,7 @@ class TimerService : Service() {
} }
private fun setDoNotDisturb(doNotDisturb: Boolean) { private fun setDoNotDisturb(doNotDisturb: Boolean) {
if (notificationManagerService.isNotificationPolicyAccessGranted()) { if (timerRepository.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)

View File

@@ -104,6 +104,7 @@ fun SettingsScreenRoot(
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true) val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true) val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound) val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle() val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
@@ -126,11 +127,13 @@ fun SettingsScreenRoot(
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled, alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled, vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled,
alarmSound = alarmSound, alarmSound = alarmSound,
onAlarmEnabledChange = viewModel::saveAlarmEnabled, onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled, onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme, onBlackThemeChange = viewModel::saveBlackTheme,
onAodEnabledChange = viewModel::saveAodEnabled, onAodEnabledChange = viewModel::saveAodEnabled,
onDndEnabledChange = viewModel::saveDndEnabled,
onAlarmSoundChanged = { onAlarmSoundChanged = {
viewModel.saveAlarmSound(it) viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply { Intent(context, TimerService::class.java).apply {
@@ -155,11 +158,13 @@ private fun SettingsScreen(
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
alarmEnabled: Boolean, alarmEnabled: Boolean,
vibrateEnabled: Boolean, vibrateEnabled: Boolean,
dndEnabled: Boolean,
alarmSound: String, alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit, onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit, onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit, onBlackThemeChange: (Boolean) -> Unit,
onAodEnabledChange: (Boolean) -> Unit, onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit, onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit, onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit, onColorSchemeChange: (Color) -> Unit,
@@ -270,11 +275,13 @@ private fun SettingsScreen(
entry<Screen.Settings.Timer> { entry<Screen.Settings.Timer> {
TimerSettings( TimerSettings(
aodEnabled = preferencesState.aodEnabled, aodEnabled = preferencesState.aodEnabled,
dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange, onAodEnabledChange = onAodEnabledChange,
onDndEnabledChange = onDndEnabledChange,
onBack = backStack::removeLastOrNull onBack = backStack::removeLastOrNull
) )
} }

View File

@@ -17,6 +17,11 @@
package org.nsh07.pomodoro.ui.settingsScreen.screens package org.nsh07.pomodoro.ui.settingsScreen.screens
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -31,6 +36,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
@@ -51,6 +57,7 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -59,6 +66,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -76,19 +84,58 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun TimerSettings( fun TimerSettings(
aodEnabled: Boolean, aodEnabled: Boolean,
dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit, onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
val appName = stringResource(R.string.app_name)
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false)
}
val switchItems = listOf(
SettingsSwitchItem(
checked = dndEnabled,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
onClick = {
if (it && !notificationManagerService.isNotificationPolicyAccessGranted()) {
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
Toast.makeText(context, "Enable permission for \"$appName\"", Toast.LENGTH_LONG)
.show()
context.startActivity(intent)
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
onDndEnabledChange(it)
}
),
SettingsSwitchItem(
checked = aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
)
)
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar( LargeFlexibleTopAppBar(
@@ -213,14 +260,7 @@ fun TimerSettings(
) )
} }
item { Spacer(Modifier.height(12.dp)) } item { Spacer(Modifier.height(12.dp)) }
item { itemsIndexed(switchItems) { index, item ->
val item = SettingsSwitchItem(
checked = aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
)
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
@@ -254,7 +294,13 @@ fun TimerSettings(
) )
}, },
colors = listItemColors, colors = listItemColors,
modifier = Modifier.clip(cardShape) modifier = Modifier.clip(
when (index) {
0 -> topListItemShape
switchItems.size - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
) )
} }
@@ -311,7 +357,9 @@ private fun TimerSettingsPreview() {
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
aodEnabled = true, aodEnabled = true,
dndEnabled = false,
onBack = {}, onBack = {},
onAodEnabledChange = {} onAodEnabledChange = {},
onDndEnabledChange = {}
) )
} }

View File

@@ -85,6 +85,8 @@ class SettingsViewModel(
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged() preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled = val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged() preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
val dndEnabled =
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -179,6 +181,13 @@ class SettingsViewModel(
} }
} }
fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.dndEnabled = enabled
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
}
}
fun saveAlarmSound(uri: Uri?) { fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri timerRepository.alarmSoundUri = uri

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
@@ -85,6 +95,9 @@ class TimerViewModel(
timerRepository.vibrateEnabled = timerRepository.vibrateEnabled =
preferenceRepository.getBooleanPreference("vibrate_enabled") preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true) ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
timerRepository.dndEnabled =
preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
timerRepository.alarmSoundUri = ( timerRepository.alarmSoundUri = (
preferenceRepository.getStringPreference("alarm_sound") preferenceRepository.getStringPreference("alarm_sound")

View File

@@ -0,0 +1,26 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M320,520h320q17,0 28.5,-11.5T680,480q0,-17 -11.5,-28.5T640,440L320,440q-17,0 -28.5,11.5T280,480q0,17 11.5,28.5T320,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

View File

@@ -79,4 +79,6 @@
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="durations">Durations</string> <string name="durations">Durations</string>
<string name="sound">Sound</string> <string name="sound">Sound</string>
<string name="dnd">Do Not Disturb</string>
<string name="dnd_desc">Turn on DND when running a Focus timer</string>
</resources> </resources>