From 3c86c2ce38fddadb2b920926b3b6cd8936cbb10f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 4 Dec 2025 10:58:16 +0530 Subject: [PATCH] feat(alarm): add option to only play alarm on headphones Closes: #134 --- .../nsh07/pomodoro/service/TimerService.kt | 8 +- .../ui/settingsScreen/SettingsSwitchItem.kt | 1 + .../settingsScreen/screens/AlarmSettings.kt | 141 +++++++++++------- .../viewModel/SettingsAction.kt | 1 + .../settingsScreen/viewModel/SettingsState.kt | 1 + .../viewModel/SettingsViewModel.kt | 22 ++- app/src/main/res/drawable/music_note.xml | 26 ++++ app/src/main/res/values/strings.xml | 2 + 8 files changed, 147 insertions(+), 55 deletions(-) create mode 100644 app/src/main/res/drawable/music_note.xml 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 bffa6cd..3948133 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -450,15 +450,19 @@ class TimerService : Service() { } private fun initializeMediaPlayer(): MediaPlayer? { + val settingsState = _settingsState.value return try { MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_ALARM) + .setUsage( + if (settingsState.mediaVolumeForAlarm) AudioAttributes.USAGE_MEDIA + else AudioAttributes.USAGE_ALARM + ) .build() ) - _settingsState.value.alarmSoundUri?.let { + settingsState.alarmSoundUri?.let { setDataSource(applicationContext, it) prepare() } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt index f16c2f4..e202910 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt @@ -23,6 +23,7 @@ import androidx.annotation.StringRes data class SettingsSwitchItem( val checked: Boolean, val enabled: Boolean = true, + val collapsible: Boolean = false, @param:DrawableRes val icon: Int, @param:StringRes val label: Int, @param:StringRes val description: Int, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index e0e2fd9..fdd7ea4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -25,6 +25,7 @@ import android.net.Uri import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -43,6 +44,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults @@ -60,8 +62,10 @@ 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.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.nsh07.pomodoro.R @@ -74,6 +78,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors 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 @@ -127,25 +132,36 @@ fun AlarmSettings( } val switchItems = remember( - settingsState.blackTheme, - settingsState.aodEnabled, settingsState.alarmEnabled, - settingsState.vibrateEnabled + settingsState.vibrateEnabled, + settingsState.mediaVolumeForAlarm ) { listOf( - SettingsSwitchItem( - checked = settingsState.alarmEnabled, - icon = R.drawable.alarm_on, - label = R.string.sound, - description = R.string.alarm_desc, - onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) } + listOf( + SettingsSwitchItem( + checked = settingsState.alarmEnabled, + icon = R.drawable.alarm_on, + label = R.string.sound, + description = R.string.alarm_desc, + onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) } + ), + SettingsSwitchItem( + checked = settingsState.vibrateEnabled, + icon = R.drawable.mobile_vibrate, + label = R.string.vibrate, + description = R.string.vibrate_desc, + onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) } + ) ), - SettingsSwitchItem( - checked = settingsState.vibrateEnabled, - icon = R.drawable.mobile_vibrate, - label = R.string.vibrate, - description = R.string.vibrate_desc, - onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) } + listOf( + SettingsSwitchItem( + checked = settingsState.mediaVolumeForAlarm, + collapsible = true, + icon = R.drawable.music_note, + label = R.string.media_volume_for_alarm, + description = R.string.media_volume_for_alarm_desc, + onClick = { onAction(SettingsAction.SaveMediaVolumeForAlarm(it)) } + ) ) ) } @@ -203,44 +219,65 @@ fun AlarmSettings( .clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) }) ) } - itemsIndexed(switchItems) { index, item -> - ListItem( - leadingContent = { - Icon(painterResource(item.icon), contentDescription = null) - }, - headlineContent = { Text(stringResource(item.label)) }, - supportingContent = { Text(stringResource(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) { - switchItems.lastIndex -> bottomListItemShape - else -> middleListItemShape + switchItems.fastForEach { items -> + itemsIndexed(items) { index, item -> + ListItem( + leadingContent = { + Icon(painterResource(item.icon), contentDescription = null) + }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { + if (item.collapsible) { + var expanded by remember { mutableStateOf(false) } + Text( + stringResource(item.description), + maxLines = if (expanded) Int.MAX_VALUE else 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .clickable { expanded = !expanded } + .animateContentSize(motionScheme.defaultSpatialSpec()) + ) + } else { + Text(stringResource(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 { + items.size == 1 -> cardShape + index == items.lastIndex -> bottomListItemShape + else -> middleListItemShape + } + ) + ) + } + + item { + Spacer(Modifier.height(12.dp)) + } } item { Spacer(Modifier.height(12.dp)) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt index ae5541a..56e9549 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt @@ -26,6 +26,7 @@ sealed interface SettingsAction { data class SaveBlackTheme(val enabled: Boolean) : SettingsAction data class SaveAodEnabled(val enabled: Boolean) : SettingsAction data class SaveDndEnabled(val enabled: Boolean) : SettingsAction + data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction data class SaveAlarmSound(val uri: Uri?) : SettingsAction data class SaveTheme(val theme: String) : SettingsAction data class SaveColorScheme(val color: Color) : SettingsAction diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt index 31ddc7c..8b74711 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -31,6 +31,7 @@ data class SettingsState( val alarmEnabled: Boolean = true, val vibrateEnabled: Boolean = true, val dndEnabled: Boolean = false, + val mediaVolumeForAlarm: Boolean = false, val focusTime: Long = 25 * 60 * 1000L, val shortBreakTime: Long = 5 * 60 * 1000L, 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 a637111..d9a3fbe 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 @@ -109,6 +109,7 @@ class SettingsViewModel( is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled) is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled) is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled) + is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled) is SettingsAction.SaveColorScheme -> saveColorScheme(action.color) is SettingsAction.SaveTheme -> saveTheme(action.theme) is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled) @@ -260,6 +261,18 @@ class SettingsViewModel( } } + private fun saveMediaVolumeForAlarm(mediaVolumeForAlarm: Boolean) { + viewModelScope.launch { + _settingsState.update { currentState -> + currentState.copy(mediaVolumeForAlarm = mediaVolumeForAlarm) + } + preferenceRepository.saveBooleanPreference( + "media_volume_for_alarm", + mediaVolumeForAlarm + ) + } + } + suspend fun reloadSettings() { var settingsState = _settingsState.value val focusTime = @@ -316,6 +329,12 @@ class SettingsViewModel( ) val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled") ?: preferenceRepository.saveBooleanPreference("dnd_enabled", settingsState.dndEnabled) + val mediaVolumeForAlarm = + preferenceRepository.getBooleanPreference("media_volume_for_alarm") + ?: preferenceRepository.saveBooleanPreference( + "media_volume_for_alarm", + settingsState.mediaVolumeForAlarm + ) _settingsState.update { currentState -> currentState.copy( @@ -330,7 +349,8 @@ class SettingsViewModel( aodEnabled = aodEnabled, alarmEnabled = alarmEnabled, vibrateEnabled = vibrateEnabled, - dndEnabled = dndEnabled + dndEnabled = dndEnabled, + mediaVolumeForAlarm = mediaVolumeForAlarm ) } diff --git a/app/src/main/res/drawable/music_note.xml b/app/src/main/res/drawable/music_note.xml new file mode 100644 index 0000000..84eb1e9 --- /dev/null +++ b/app/src/main/res/drawable/music_note.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 112488e..441470e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,4 +103,6 @@ Support me with a small donation Customize further with Tomato+ License + Headphone mode + Plays on headphones only. If headphones are disconnected, alarm plays through speaker at media volume. \ No newline at end of file