diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2242c..d9f3776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ +## 1.9.0 – Unreleased + +### New Feature + +- Provide audible or haptic feedback on click + ## [1.8.8](https://github.com/ldeso/blitz/releases/tag/v1.8.8) – 2024-05-21 This release improves the style of the text, enables hardware memory tagging on compatible devices and updates dependencies. diff --git a/metadata/en-US/changelogs/190.txt b/metadata/en-US/changelogs/190.txt new file mode 100644 index 0000000..da22ac1 --- /dev/null +++ b/metadata/en-US/changelogs/190.txt @@ -0,0 +1 @@ +• Provide audible or haptic feedback on click \ No newline at end of file diff --git a/src/main/kotlin/ui/ClockInputs.kt b/src/main/kotlin/ui/ClockInputs.kt index 4085752..c2d444e 100644 --- a/src/main/kotlin/ui/ClockInputs.kt +++ b/src/main/kotlin/ui/ClockInputs.kt @@ -5,6 +5,9 @@ package net.leodesouza.blitz.ui import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.media.AudioManager +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_VIBRATE import android.os.Build import androidx.activity.compose.BackHandler import androidx.activity.compose.PredictiveBackHandler @@ -15,6 +18,8 @@ import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType @@ -108,6 +113,8 @@ fun ClockBackHandler( * @param[isBusyProvider] Lambda for whether the clock is currently busy. * @param[displayOrientation] The [ORIENTATION_PORTRAIT] or [ORIENTATION_LANDSCAPE] of the display. * @param[layoutDirection] Whether the layout direction is left-to-right or right-to-left. + * @param[audioManager] The interface used to play sound effects. + * @param[haptics] The interface used to provide haptic feedback. * @param[start] Callback called to start the clock. * @param[play] Callback called to switch to the next player. * @param[save] Callback called to save the time or configuration. @@ -121,6 +128,8 @@ fun Modifier.clockInput( isBusyProvider: () -> Boolean, displayOrientation: Int, // ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE layoutDirection: LayoutDirection, + audioManager: AudioManager, + haptics: HapticFeedback, start: () -> Unit, play: () -> Unit, save: () -> Unit, @@ -130,6 +139,8 @@ fun Modifier.clockInput( onClickEvent( clockState = clockStateProvider(), isBusy = isBusyProvider(), + audioManager = audioManager, + haptics = haptics, start = start, play = play, ) @@ -153,7 +164,11 @@ fun Modifier.clockInput( }, onDragEnd = { onDragEnd( - clockState = clockStateProvider(), isBusy = isBusyProvider(), play = play, + clockState = clockStateProvider(), + isBusy = isBusyProvider(), + audioManager = audioManager, + haptics = haptics, + play = play, ) }, onHorizontalDrag = { _: PointerInputChange, dragAmount: Float -> @@ -178,7 +193,11 @@ fun Modifier.clockInput( }, onDragEnd = { onDragEnd( - clockState = clockStateProvider(), isBusy = isBusyProvider(), play = play, + clockState = clockStateProvider(), + isBusy = isBusyProvider(), + audioManager = audioManager, + haptics = haptics, + play = play, ) }, onVerticalDrag = { _: PointerInputChange, dragAmount: Float -> @@ -201,17 +220,32 @@ fun Modifier.clockInput( * * @param[clockState] Current state of the clock. * @param[isBusy] Whether the clock is currently busy. + * @param[audioManager] The interface used to play sound effects. + * @param[haptics] The interface used to provide haptic feedback. * @param[start] Callback called to start the clock. * @param[play] Callback called to switch to the next player. */ private fun onClickEvent( - clockState: ClockState, isBusy: Boolean, start: () -> Unit, play: () -> Unit, + clockState: ClockState, + isBusy: Boolean, + audioManager: AudioManager, + haptics: HapticFeedback, + start: () -> Unit, + play: () -> Unit, ) { if (!isBusy) { when (clockState) { - ClockState.PAUSED, ClockState.SOFT_RESET, ClockState.FULL_RESET -> start() - ClockState.TICKING -> play() - else -> Unit + ClockState.PAUSED, ClockState.SOFT_RESET, ClockState.FULL_RESET -> run { + start() + provideFeedback(audioManager, haptics) + } + + ClockState.TICKING -> run { + play() + provideFeedback(audioManager, haptics) + } + + ClockState.FINISHED -> Unit } } } @@ -286,11 +320,20 @@ private fun onDragStart(clockState: ClockState, isBusy: Boolean, save: () -> Uni * * @param[clockState] Current state of the clock. * @param[isBusy] Whether the clock is currently busy. + * @param[audioManager] The interface used to play sound effects. + * @param[haptics] The interface used to provide haptic feedback. * @param[play] Callback called to switch to the next player. */ -private fun onDragEnd(clockState: ClockState, isBusy: Boolean, play: () -> Unit) { +private fun onDragEnd( + clockState: ClockState, + isBusy: Boolean, + audioManager: AudioManager, + haptics: HapticFeedback, + play: () -> Unit, +) { if (!isBusy && clockState == ClockState.TICKING) { play() + provideFeedback(audioManager, haptics) } } @@ -388,3 +431,24 @@ private fun onVerticalDrag( } } } + +/** + * Use the [audioManager] interface to play a sound effect when the ringtone mode is set to + * [RINGER_MODE_NORMAL], or use the [haptics] interface to provide haptic feedback when + * the ringtone mode is set to [RINGER_MODE_VIBRATE]. + */ +private fun provideFeedback(audioManager: AudioManager, haptics: HapticFeedback) { + when (audioManager.ringerMode) { + RINGER_MODE_NORMAL -> with(audioManager) { + val streamType = AudioManager.STREAM_MUSIC + val volumeIndex = getStreamVolume(streamType).toFloat() + if (volumeIndex > 0F) { + val maxVolumeIndex = getStreamMaxVolume(streamType).toFloat() + val volume = volumeIndex / maxVolumeIndex + playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, volume) + } + } + + RINGER_MODE_VIBRATE -> haptics.performHapticFeedback(HapticFeedbackType.LongPress) + } +} diff --git a/src/main/kotlin/ui/ClockScreen.kt b/src/main/kotlin/ui/ClockScreen.kt index 4293c97..6219137 100644 --- a/src/main/kotlin/ui/ClockScreen.kt +++ b/src/main/kotlin/ui/ClockScreen.kt @@ -3,6 +3,8 @@ package net.leodesouza.blitz.ui +import android.content.Context +import android.media.AudioManager import androidx.activity.BackEventCompat import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -16,6 +18,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.LayoutDirection @@ -55,6 +59,8 @@ fun ClockScreen( val lifecycleOwner = LocalLifecycleOwner.current val displayOrientation = LocalConfiguration.current.orientation val layoutDirection = LocalLayoutDirection.current + val audioManager = LocalContext.current.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val haptics = LocalHapticFeedback.current val whiteTime by clockViewModel.whiteTime.collectAsStateWithLifecycle(lifecycleOwner) val blackTime by clockViewModel.blackTime.collectAsStateWithLifecycle(lifecycleOwner) @@ -113,6 +119,8 @@ fun ClockScreen( isBusyProvider = { isBusy }, displayOrientation = displayOrientation, layoutDirection = layoutDirection, + audioManager = audioManager, + haptics = haptics, start = { onClockStart() clockViewModel.start()