feat(settings): implement a settings option to enable auto-dnd
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* 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
|
||||
@@ -27,6 +37,7 @@ interface TimerRepository {
|
||||
|
||||
var alarmEnabled: Boolean
|
||||
var vibrateEnabled: Boolean
|
||||
var dndEnabled: Boolean
|
||||
|
||||
var colorScheme: ColorScheme
|
||||
|
||||
@@ -46,6 +57,7 @@ class AppTimerRepository : TimerRepository {
|
||||
override var timerFrequency: Float = 10f
|
||||
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
|
||||
|
||||
@@ -129,7 +129,7 @@ class TimerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
Actions.SKIP.toString() -> skipTimer(true)
|
||||
Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) }
|
||||
|
||||
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()
|
||||
skipScope.launch {
|
||||
saveTimeToDb()
|
||||
updateProgressSegments()
|
||||
showTimerNotification(0, paused = true, complete = !fromButton)
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
saveTimeToDb()
|
||||
updateProgressSegments()
|
||||
showTimerNotification(0, paused = true, complete = !fromButton)
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
|
||||
cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
|
||||
cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
|
||||
|
||||
if (cycles % 2 == 0) {
|
||||
if (timerState.value.timerRunning) setDoNotDisturb(true)
|
||||
time = timerRepository.focusTime
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = TimerMode.FOCUS,
|
||||
timeStr = millisecondsToStr(time),
|
||||
totalTime = time,
|
||||
nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
|
||||
timerRepository.longBreakTime
|
||||
) else millisecondsToStr(
|
||||
timerRepository.shortBreakTime
|
||||
),
|
||||
currentFocusCount = cycles / 2 + 1,
|
||||
totalFocusCount = timerRepository.sessionLength
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (timerState.value.timerRunning) setDoNotDisturb(false)
|
||||
val long = cycles == (timerRepository.sessionLength * 2) - 1
|
||||
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
|
||||
if (cycles % 2 == 0) {
|
||||
if (timerState.value.timerRunning) setDoNotDisturb(true)
|
||||
time = timerRepository.focusTime
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = TimerMode.FOCUS,
|
||||
timeStr = millisecondsToStr(time),
|
||||
totalTime = time,
|
||||
nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
|
||||
timerRepository.longBreakTime
|
||||
) else millisecondsToStr(
|
||||
timerRepository.shortBreakTime
|
||||
),
|
||||
currentFocusCount = cycles / 2 + 1,
|
||||
totalFocusCount = timerRepository.sessionLength
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (timerState.value.timerRunning) setDoNotDisturb(false)
|
||||
val long = cycles == (timerRepository.sessionLength * 2) - 1
|
||||
time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
|
||||
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
timeStr = millisecondsToStr(time),
|
||||
totalTime = time,
|
||||
nextTimerMode = TimerMode.FOCUS,
|
||||
nextTimeStr = millisecondsToStr(timerRepository.focusTime)
|
||||
)
|
||||
}
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
timeStr = millisecondsToStr(time),
|
||||
totalTime = time,
|
||||
nextTimerMode = TimerMode.FOCUS,
|
||||
nextTimeStr = millisecondsToStr(timerRepository.focusTime)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,7 +447,7 @@ class TimerService : Service() {
|
||||
}
|
||||
|
||||
private fun setDoNotDisturb(doNotDisturb: Boolean) {
|
||||
if (notificationManagerService.isNotificationPolicyAccessGranted()) {
|
||||
if (timerRepository.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) {
|
||||
if (doNotDisturb) {
|
||||
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS)
|
||||
} else notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
|
||||
|
||||
@@ -104,6 +104,7 @@ fun SettingsScreenRoot(
|
||||
|
||||
val alarmEnabled by viewModel.alarmEnabled.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 preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
|
||||
@@ -126,11 +127,13 @@ fun SettingsScreenRoot(
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
alarmEnabled = alarmEnabled,
|
||||
vibrateEnabled = vibrateEnabled,
|
||||
dndEnabled = dndEnabled,
|
||||
alarmSound = alarmSound,
|
||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||
onBlackThemeChange = viewModel::saveBlackTheme,
|
||||
onAodEnabledChange = viewModel::saveAodEnabled,
|
||||
onDndEnabledChange = viewModel::saveDndEnabled,
|
||||
onAlarmSoundChanged = {
|
||||
viewModel.saveAlarmSound(it)
|
||||
Intent(context, TimerService::class.java).apply {
|
||||
@@ -155,11 +158,13 @@ private fun SettingsScreen(
|
||||
sessionsSliderState: SliderState,
|
||||
alarmEnabled: Boolean,
|
||||
vibrateEnabled: Boolean,
|
||||
dndEnabled: Boolean,
|
||||
alarmSound: String,
|
||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||
onBlackThemeChange: (Boolean) -> Unit,
|
||||
onAodEnabledChange: (Boolean) -> Unit,
|
||||
onDndEnabledChange: (Boolean) -> Unit,
|
||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||
onThemeChange: (String) -> Unit,
|
||||
onColorSchemeChange: (Color) -> Unit,
|
||||
@@ -270,11 +275,13 @@ private fun SettingsScreen(
|
||||
entry<Screen.Settings.Timer> {
|
||||
TimerSettings(
|
||||
aodEnabled = preferencesState.aodEnabled,
|
||||
dndEnabled = dndEnabled,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
onAodEnabledChange = onAodEnabledChange,
|
||||
onDndEnabledChange = onDndEnabledChange,
|
||||
onBack = backStack::removeLastOrNull
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
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.foundation.background
|
||||
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.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -59,6 +66,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.topListItemShape
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun TimerSettings(
|
||||
aodEnabled: Boolean,
|
||||
dndEnabled: Boolean,
|
||||
focusTimeInputFieldState: TextFieldState,
|
||||
shortBreakTimeInputFieldState: TextFieldState,
|
||||
longBreakTimeInputFieldState: TextFieldState,
|
||||
sessionsSliderState: SliderState,
|
||||
onAodEnabledChange: (Boolean) -> Unit,
|
||||
onDndEnabledChange: (Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
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)) {
|
||||
LargeFlexibleTopAppBar(
|
||||
@@ -213,14 +260,7 @@ fun TimerSettings(
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
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
|
||||
)
|
||||
itemsIndexed(switchItems) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
@@ -254,7 +294,13 @@ fun TimerSettings(
|
||||
)
|
||||
},
|
||||
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,
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
aodEnabled = true,
|
||||
dndEnabled = false,
|
||||
onBack = {},
|
||||
onAodEnabledChange = {}
|
||||
onAodEnabledChange = {},
|
||||
onDndEnabledChange = {}
|
||||
)
|
||||
}
|
||||
@@ -85,6 +85,8 @@ class SettingsViewModel(
|
||||
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
|
||||
val vibrateEnabled =
|
||||
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
|
||||
val dndEnabled =
|
||||
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
|
||||
|
||||
init {
|
||||
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?) {
|
||||
viewModelScope.launch {
|
||||
timerRepository.alarmSoundUri = uri
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* 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.ui.timerScreen.viewModel
|
||||
@@ -85,6 +95,9 @@ class TimerViewModel(
|
||||
timerRepository.vibrateEnabled =
|
||||
preferenceRepository.getBooleanPreference("vibrate_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
|
||||
timerRepository.dndEnabled =
|
||||
preferenceRepository.getBooleanPreference("dnd_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
|
||||
|
||||
timerRepository.alarmSoundUri = (
|
||||
preferenceRepository.getStringPreference("alarm_sound")
|
||||
|
||||
26
app/src/main/res/drawable/dnd.xml
Normal file
26
app/src/main/res/drawable/dnd.xml
Normal 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>
|
||||
@@ -79,4 +79,6 @@
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="durations">Durations</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>
|
||||
Reference in New Issue
Block a user