From 6fd8ee43be207645d09a6e5717faa0e8f1942871 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 15 Sep 2025 11:37:27 +0530 Subject: [PATCH] feat: Add options in the settings menu to disable alarm and vibration #36 --- .../pomodoro/data/PreferenceRepository.kt | 4 +- .../nsh07/pomodoro/data/TimerRepository.kt | 4 + .../nsh07/pomodoro/service/TimerService.kt | 29 +++-- .../ui/settingsScreen/SettingsScreen.kt | 117 ++++++++++++++++-- .../viewModel/SettingsViewModel.kt | 27 ++++ .../java/org/nsh07/pomodoro/ui/theme/Shape.kt | 43 +++++++ .../timerScreen/viewModel/TimerViewModel.kt | 12 ++ app/src/main/res/drawable/alarm_on.xml | 16 +++ app/src/main/res/drawable/check.xml | 16 +++ app/src/main/res/drawable/clear.xml | 16 +++ app/src/main/res/drawable/mobile_vibrate.xml | 16 +++ 11 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt create mode 100644 app/src/main/res/drawable/alarm_on.xml create mode 100644 app/src/main/res/drawable/check.xml create mode 100644 app/src/main/res/drawable/clear.xml create mode 100644 app/src/main/res/drawable/mobile_vibrate.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt index 438af3f..d9abc83 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext /** * Interface for reading/writing app preferences to the app's database. This style of storage aims - * to mimic the Preferences DataStore library, preventing the requirement of migration if the - * database schema changes. + * to mimic the Preferences DataStore library, preventing the requirement of migration if new + * preferences are added */ interface PreferenceRepository { /** diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt index 067a783..e7284e6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt @@ -17,6 +17,8 @@ interface TimerRepository { var longBreakTime: Long var sessionLength: Int var timerFrequency: Float + var alarmEnabled: Boolean + var vibrateEnabled: Boolean } /** @@ -28,4 +30,6 @@ class AppTimerRepository : TimerRepository { override var longBreakTime = 15 * 60 * 1000L override var sessionLength = 4 override var timerFrequency: Float = 10f + override var alarmEnabled = true + override var vibrateEnabled = true } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 4e775a0..234e276 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -344,23 +344,28 @@ class TimerService : Service() { } fun startAlarm() { - alarm.start() + if (timerRepository.alarmEnabled) alarm.start() - if (!vibrator.hasVibrator()) { - return + if (timerRepository.vibrateEnabled) { + if (!vibrator.hasVibrator()) { + return + } + val vibrationPattern = longArrayOf(0, 1000, 1000, 1000) + val repeat = 2 + val effect = VibrationEffect.createWaveform(vibrationPattern, repeat) + vibrator.vibrate(effect) } - - val vibrationPattern = longArrayOf(0, 1000, 1000, 1000) - val repeat = 2 - - val effect = VibrationEffect.createWaveform(vibrationPattern, repeat) - vibrator.vibrate(effect) } fun stopAlarm() { - alarm.pause() - alarm.seekTo(0) - vibrator.cancel() + if (timerRepository.alarmEnabled) { + alarm.pause() + alarm.seekTo(0) + } + + if (timerRepository.vibrateEnabled) { + vibrator.cancel() + } _timerState.update { currentState -> currentState.copy(alarmRinging = false) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 5355f9c..b3683fe 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -7,6 +7,7 @@ package org.nsh07.pomodoro.ui.settingsScreen +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll @@ -18,8 +19,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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 @@ -32,10 +35,11 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.ListItem import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.shapes import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Slider import androidx.compose.material3.SliderState +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -56,12 +60,17 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import org.nsh07.pomodoro.R import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape import org.nsh07.pomodoro.ui.theme.TomatoTheme @OptIn(ExperimentalMaterial3Api::class) @@ -80,6 +89,9 @@ fun SettingsScreenRoot( viewModel.longBreakTimeTextFieldState } + val alarmEnabled = viewModel.alarmEnabled.collectAsStateWithLifecycle() + val vibrateEnabled = viewModel.vibrateEnabled.collectAsStateWithLifecycle() + val sessionsSliderState = rememberSaveable( saver = SliderState.Saver( viewModel.sessionsSliderState.onValueChangeFinished, @@ -94,6 +106,10 @@ fun SettingsScreenRoot( shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, sessionsSliderState = sessionsSliderState, + alarmEnabled = alarmEnabled.value, + vibrateEnabled = vibrateEnabled.value, + onAlarmEnabledChange = viewModel::saveAlarmEnabled, + onVibrateEnabledChange = viewModel::saveVibrateEnabled, modifier = modifier ) } @@ -105,9 +121,35 @@ private fun SettingsScreen( shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, sessionsSliderState: SliderState, + alarmEnabled: Boolean, + vibrateEnabled: Boolean, + onAlarmEnabledChange: (Boolean) -> Unit, + onVibrateEnabledChange: (Boolean) -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val switchColors = SwitchDefaults.colors( + checkedIconColor = colorScheme.primary, + ) + + val switchItems = remember(alarmEnabled, vibrateEnabled) { + listOf( + SettingsSwitchItem( + checked = alarmEnabled, + icon = R.drawable.alarm_on, + label = "Alarm", + description = "Ring your system alarm sound when a timer completes", + onClick = onAlarmEnabledChange + ), + SettingsSwitchItem( + checked = vibrateEnabled, + icon = R.drawable.mobile_vibrate, + label = "Vibrate", + description = "Vibrate in a repeating pattern when a timer completes", + onClick = onVibrateEnabledChange + ) + ) + } Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { TopAppBar( @@ -155,10 +197,10 @@ private fun SettingsScreen( MinuteInputField( state = focusTimeInputFieldState, shape = RoundedCornerShape( - topStart = 16.dp, - bottomStart = 16.dp, - topEnd = 4.dp, - bottomEnd = 4.dp + topStart = topListItemShape.topStart, + bottomStart = topListItemShape.topStart, + topEnd = topListItemShape.bottomStart, + bottomEnd = topListItemShape.bottomStart ), imeAction = ImeAction.Next ) @@ -174,7 +216,7 @@ private fun SettingsScreen( ) MinuteInputField( state = shortBreakTimeInputFieldState, - shape = RoundedCornerShape(4.dp), + shape = RoundedCornerShape(middleListItemShape.topStart), imeAction = ImeAction.Next ) } @@ -190,10 +232,10 @@ private fun SettingsScreen( MinuteInputField( state = longBreakTimeInputFieldState, shape = RoundedCornerShape( - topStart = 4.dp, - bottomStart = 4.dp, - topEnd = 16.dp, - bottomEnd = 16.dp + topStart = bottomListItemShape.topStart, + bottomStart = bottomListItemShape.topStart, + topEnd = bottomListItemShape.bottomStart, + bottomEnd = bottomListItemShape.bottomStart ), imeAction = ImeAction.Done ) @@ -224,7 +266,48 @@ private fun SettingsScreen( } }, colors = listItemColors, - modifier = Modifier.clip(shapes.large) + modifier = Modifier.clip(cardShape) + ) + } + item { Spacer(Modifier.height(12.dp)) } + itemsIndexed(switchItems) { index, item -> + ListItem( + leadingContent = { + Icon(painterResource(item.icon), contentDescription = null) + }, + headlineContent = { Text(item.label) }, + supportingContent = { Text(item.description) }, + trailingContent = { + Switch( + checked = item.checked, + onCheckedChange = { item.onClick(it) }, + thumbContent = { + if (item.checked) { + Icon( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(R.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + modifier = Modifier + .clip( + when (index) { + 0 -> topListItemShape + switchItems.lastIndex -> bottomListItemShape + else -> middleListItemShape + } + ) ) } item { @@ -275,7 +358,19 @@ fun SettingsScreenPreview() { shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()), longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()), sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f), + alarmEnabled = true, + vibrateEnabled = true, + onAlarmEnabledChange = {}, + onVibrateEnabledChange = {}, modifier = Modifier.fillMaxSize() ) } } + +data class SettingsSwitchItem( + val checked: Boolean, + @DrawableRes val icon: Int, + val label: String, + val description: String, + val onClick: (Boolean) -> Unit +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index c3454e5..af545ca 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -19,6 +19,9 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import org.nsh07.pomodoro.TomatoApplication @@ -44,6 +47,14 @@ class SettingsViewModel( onValueChangeFinished = ::updateSessionLength ) + private val _alarmEnabled: MutableStateFlow = + MutableStateFlow(timerRepository.alarmEnabled) + val alarmEnabled: StateFlow = _alarmEnabled.asStateFlow() + + private val _vibrateEnabled: MutableStateFlow = + MutableStateFlow(timerRepository.alarmEnabled) + val vibrateEnabled: StateFlow = _vibrateEnabled.asStateFlow() + init { viewModelScope.launch(Dispatchers.IO) { snapshotFlow { focusTimeTextFieldState.text } @@ -92,6 +103,22 @@ class SettingsViewModel( } } + fun saveAlarmEnabled(enabled: Boolean) { + viewModelScope.launch { + timerRepository.alarmEnabled = preferenceRepository + .saveIntPreference("alarm_enabled", if (enabled) 1 else 0) == 1 + _alarmEnabled.value = enabled + } + } + + fun saveVibrateEnabled(enabled: Boolean) { + viewModelScope.launch { + timerRepository.vibrateEnabled = preferenceRepository + .saveIntPreference("vibrate_enabled", if (enabled) 1 else 0) == 1 + _vibrateEnabled.value = enabled + } + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt new file mode 100644 index 0000000..b7dea55 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.ui.theme + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme.shapes +import androidx.compose.runtime.Composable + +object TomatoShapeDefaults { + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + val topListItemShape: RoundedCornerShape + @Composable get() = + RoundedCornerShape( + topStart = shapes.largeIncreased.topStart, + topEnd = shapes.largeIncreased.topEnd, + bottomStart = shapes.extraSmall.bottomStart, + bottomEnd = shapes.extraSmall.bottomStart + ) + + val middleListItemShape: RoundedCornerShape + @Composable get() = RoundedCornerShape(shapes.extraSmall.topStart) + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + val bottomListItemShape: RoundedCornerShape + @Composable get() = + RoundedCornerShape( + topStart = shapes.extraSmall.topStart, + topEnd = shapes.extraSmall.topEnd, + bottomStart = shapes.largeIncreased.bottomStart, + bottomEnd = shapes.largeIncreased.bottomEnd + ) + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + val cardShape: CornerBasedShape + @Composable get() = shapes.largeIncreased +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index a173dd4..6149b44 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -77,6 +77,18 @@ class TimerViewModel( timerRepository.sessionLength ) + timerRepository.alarmEnabled = (preferenceRepository.getIntPreference("alarm_enabled") + ?: preferenceRepository.saveIntPreference( + "alarm_enabled", + 1 + )) == 1 + timerRepository.vibrateEnabled = + (preferenceRepository.getIntPreference("vibrate_enabled") + ?: preferenceRepository.saveIntPreference( + "vibrate_enabled", + 1 + )) == 1 + resetTimer() var lastDate = statRepository.getLastDate() diff --git a/app/src/main/res/drawable/alarm_on.xml b/app/src/main/res/drawable/alarm_on.xml new file mode 100644 index 0000000..da06d7f --- /dev/null +++ b/app/src/main/res/drawable/alarm_on.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml new file mode 100644 index 0000000..954c1ed --- /dev/null +++ b/app/src/main/res/drawable/check.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/clear.xml b/app/src/main/res/drawable/clear.xml new file mode 100644 index 0000000..970bfaa --- /dev/null +++ b/app/src/main/res/drawable/clear.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/mobile_vibrate.xml b/app/src/main/res/drawable/mobile_vibrate.xml new file mode 100644 index 0000000..7435b8a --- /dev/null +++ b/app/src/main/res/drawable/mobile_vibrate.xml @@ -0,0 +1,16 @@ + + + + +