From 7e73ac80e981bd7516c05ca838cbf0d981d5c5c9 Mon Sep 17 00:00:00 2001 From: darken Date: Sat, 4 Apr 2026 11:27:30 +0200 Subject: [PATCH] fix(aap): Fix stem actions volume control, reset button, and Default/No Action semantics Fix volume up/down doing nothing by using adjustSuggestedStreamVolume instead of dispatchMediaKeyEvent which ignores volume keycodes. Add reset-to-defaults button with confirmation dialog in the stem actions TopAppBar. Rename 'None' to 'Default' (firmware handles the press) and add 'No Action' (claimed but do nothing) to correctly model per-press-type claim mask semantics. Remove disableNone lock; add cross-side auto-set logic so selecting Default resets both sides and selecting an action promotes the other side from Default to No Action. --- .../eu/darken/capod/common/MediaControl.kt | 18 +++++ .../ui/stemactions/StemActionConfigScreen.kt | 70 +++++++++++++------ .../stemactions/StemActionConfigViewModel.kt | 65 ++++++++++++++--- .../monitor/core/aap/StemPressReaction.kt | 6 +- .../capod/reaction/core/stem/StemAction.kt | 1 + .../reaction/core/stem/StemActionSettings.kt | 5 ++ app/src/main/res/values/strings.xml | 5 +- 7 files changed, 137 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/eu/darken/capod/common/MediaControl.kt b/app/src/main/java/eu/darken/capod/common/MediaControl.kt index 40d83521..4d9540be 100644 --- a/app/src/main/java/eu/darken/capod/common/MediaControl.kt +++ b/app/src/main/java/eu/darken/capod/common/MediaControl.kt @@ -53,6 +53,24 @@ class MediaControl @Inject constructor( audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime + 200, eventTime + 200, KeyEvent.ACTION_UP, keyCode, 0)) } + fun adjustVolumeUp() { + log(TAG, INFO) { "adjustVolumeUp()" } + audioManager.adjustSuggestedStreamVolume( + AudioManager.ADJUST_RAISE, + AudioManager.USE_DEFAULT_STREAM_TYPE, + AudioManager.FLAG_SHOW_UI, + ) + } + + fun adjustVolumeDown() { + log(TAG, INFO) { "adjustVolumeDown()" } + audioManager.adjustSuggestedStreamVolume( + AudioManager.ADJUST_LOWER, + AudioManager.USE_DEFAULT_STREAM_TYPE, + AudioManager.FLAG_SHOW_UI, + ) + } + companion object { private val TAG = logTag("MediaControl") } diff --git a/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigScreen.kt index 7690a627..42816b2f 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.twotone.ArrowBack +import androidx.compose.material.icons.twotone.RestartAlt +import androidx.compose.material3.AlertDialog import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -21,6 +23,7 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -49,6 +52,7 @@ fun StemActionConfigScreenHost( StemActionConfigScreen( state = currentState, onNavigateUp = { vm.navUp() }, + onReset = { vm.resetAll() }, onLeftSingle = { vm.setLeftSingle(it) }, onLeftDouble = { vm.setLeftDouble(it) }, onLeftTriple = { vm.setLeftTriple(it) }, @@ -65,6 +69,7 @@ fun StemActionConfigScreenHost( fun StemActionConfigScreen( state: StemActionConfigViewModel.State, onNavigateUp: () -> Unit, + onReset: () -> Unit = {}, onLeftSingle: (StemAction) -> Unit = {}, onLeftDouble: (StemAction) -> Unit = {}, onLeftTriple: (StemAction) -> Unit = {}, @@ -74,6 +79,28 @@ fun StemActionConfigScreen( onRightTriple: (StemAction) -> Unit = {}, onRightLong: (StemAction) -> Unit = {}, ) { + var showResetDialog by remember { mutableStateOf(false) } + + if (showResetDialog) { + AlertDialog( + onDismissRequest = { showResetDialog = false }, + confirmButton = { + TextButton(onClick = { + onReset() + showResetDialog = false + }) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { showResetDialog = false }) { + Text(stringResource(android.R.string.cancel)) + } + }, + text = { Text(stringResource(R.string.stem_actions_reset_confirm_message)) }, + ) + } + Scaffold( topBar = { TopAppBar( @@ -83,6 +110,14 @@ fun StemActionConfigScreen( Icon(Icons.AutoMirrored.TwoTone.ArrowBack, contentDescription = null) } }, + actions = { + IconButton(onClick = { showResetDialog = true }) { + Icon( + imageVector = Icons.TwoTone.RestartAlt, + contentDescription = stringResource(R.string.stem_actions_reset_label), + ) + } + }, ) }, ) { paddingValues -> @@ -159,10 +194,6 @@ private fun StemActionRow( onLeftChange: (StemAction) -> Unit, onRightChange: (StemAction) -> Unit, ) { - // Enforce: if one side is non-NONE, the other can't be NONE - val leftIsLocked = rightAction != StemAction.NONE - val rightIsLocked = leftAction != StemAction.NONE - Row( modifier = Modifier .fillMaxWidth() @@ -177,7 +208,7 @@ private fun StemActionRow( StemActionDropdown( selected = leftAction, onSelected = onLeftChange, - disableNone = leftIsLocked, + otherSideAction = rightAction, modifier = Modifier.fillMaxWidth(), ) } @@ -191,7 +222,7 @@ private fun StemActionRow( StemActionDropdown( selected = rightAction, onSelected = onRightChange, - disableNone = rightIsLocked, + otherSideAction = leftAction, modifier = Modifier.fillMaxWidth(), ) } @@ -203,11 +234,17 @@ private fun StemActionRow( private fun StemActionDropdown( selected: StemAction, onSelected: (StemAction) -> Unit, - disableNone: Boolean, + otherSideAction: StemAction, modifier: Modifier = Modifier, ) { var expanded by remember { mutableStateOf(false) } + val options = if (otherSideAction == StemAction.NONE) { + StemAction.entries.filter { it != StemAction.NO_ACTION } + } else { + StemAction.entries.toList() + } + ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, @@ -227,23 +264,13 @@ private fun StemActionDropdown( expanded = expanded, onDismissRequest = { expanded = false }, ) { - for (action in StemAction.entries) { - val enabled = !(action == StemAction.NONE && disableNone) + for (action in options) { DropdownMenuItem( - text = { - Text( - text = action.label(), - color = if (enabled) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - ) - }, + text = { Text(action.label()) }, onClick = { - if (enabled) { - onSelected(action) - expanded = false - } + onSelected(action) + expanded = false }, - enabled = enabled, ) } } @@ -253,6 +280,7 @@ private fun StemActionDropdown( @Composable private fun StemAction.label(): String = when (this) { StemAction.NONE -> stringResource(R.string.stem_action_none) + StemAction.NO_ACTION -> stringResource(R.string.stem_action_no_action) StemAction.PLAY_PAUSE -> stringResource(R.string.stem_action_play_pause) StemAction.NEXT_TRACK -> stringResource(R.string.stem_action_next_track) StemAction.PREVIOUS_TRACK -> stringResource(R.string.stem_action_previous_track) diff --git a/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigViewModel.kt index 4ec992fd..567fc4d2 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/stemactions/StemActionConfigViewModel.kt @@ -6,6 +6,7 @@ import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.reaction.core.stem.StemAction import eu.darken.capod.reaction.core.stem.StemActionSettings import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import javax.inject.Inject @HiltViewModel @@ -47,12 +48,60 @@ class StemActionConfigViewModel @Inject constructor( val rightLong: StemAction = StemAction.NONE, ) - fun setLeftSingle(action: StemAction) = launch { stemActionSettings.leftSingle.update { action } } - fun setLeftDouble(action: StemAction) = launch { stemActionSettings.leftDouble.update { action } } - fun setLeftTriple(action: StemAction) = launch { stemActionSettings.leftTriple.update { action } } - fun setLeftLong(action: StemAction) = launch { stemActionSettings.leftLong.update { action } } - fun setRightSingle(action: StemAction) = launch { stemActionSettings.rightSingle.update { action } } - fun setRightDouble(action: StemAction) = launch { stemActionSettings.rightDouble.update { action } } - fun setRightTriple(action: StemAction) = launch { stemActionSettings.rightTriple.update { action } } - fun setRightLong(action: StemAction) = launch { stemActionSettings.rightLong.update { action } } + fun setLeftSingle(action: StemAction) = launch { + stemActionSettings.leftSingle.update { action } + applyCrossSideEffect(action, stemActionSettings.rightSingle) + } + + fun setLeftDouble(action: StemAction) = launch { + stemActionSettings.leftDouble.update { action } + applyCrossSideEffect(action, stemActionSettings.rightDouble) + } + + fun setLeftTriple(action: StemAction) = launch { + stemActionSettings.leftTriple.update { action } + applyCrossSideEffect(action, stemActionSettings.rightTriple) + } + + fun setLeftLong(action: StemAction) = launch { + stemActionSettings.leftLong.update { action } + applyCrossSideEffect(action, stemActionSettings.rightLong) + } + + fun setRightSingle(action: StemAction) = launch { + stemActionSettings.rightSingle.update { action } + applyCrossSideEffect(action, stemActionSettings.leftSingle) + } + + fun setRightDouble(action: StemAction) = launch { + stemActionSettings.rightDouble.update { action } + applyCrossSideEffect(action, stemActionSettings.leftDouble) + } + + fun setRightTriple(action: StemAction) = launch { + stemActionSettings.rightTriple.update { action } + applyCrossSideEffect(action, stemActionSettings.leftTriple) + } + + fun setRightLong(action: StemAction) = launch { + stemActionSettings.rightLong.update { action } + applyCrossSideEffect(action, stemActionSettings.leftLong) + } + + private suspend fun applyCrossSideEffect( + selected: StemAction, + otherSide: eu.darken.capod.common.datastore.DataStoreValue, + ) { + when (selected) { + StemAction.NONE -> otherSide.update { StemAction.NONE } + StemAction.NO_ACTION -> {} // No side-effect + else -> { + if (otherSide.flow.first() == StemAction.NONE) { + otherSide.update { StemAction.NO_ACTION } + } + } + } + } + + fun resetAll() = launch { stemActionSettings.resetAll() } } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/aap/StemPressReaction.kt b/app/src/main/java/eu/darken/capod/monitor/core/aap/StemPressReaction.kt index 977b1e3b..96e10017 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/aap/StemPressReaction.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/aap/StemPressReaction.kt @@ -50,12 +50,12 @@ class StemPressReaction @Inject constructor( } private suspend fun executeAction(action: StemAction) = when (action) { - StemAction.NONE -> Unit + StemAction.NONE, StemAction.NO_ACTION -> Unit StemAction.PLAY_PAUSE -> mediaControl.sendPlayPause() StemAction.NEXT_TRACK -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_NEXT) StemAction.PREVIOUS_TRACK -> mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS) - StemAction.VOLUME_UP -> mediaControl.sendKey(KeyEvent.KEYCODE_VOLUME_UP) - StemAction.VOLUME_DOWN -> mediaControl.sendKey(KeyEvent.KEYCODE_VOLUME_DOWN) + StemAction.VOLUME_UP -> mediaControl.adjustVolumeUp() + StemAction.VOLUME_DOWN -> mediaControl.adjustVolumeDown() } companion object { diff --git a/app/src/main/java/eu/darken/capod/reaction/core/stem/StemAction.kt b/app/src/main/java/eu/darken/capod/reaction/core/stem/StemAction.kt index 7536ed61..cccede41 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/stem/StemAction.kt +++ b/app/src/main/java/eu/darken/capod/reaction/core/stem/StemAction.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable @Serializable enum class StemAction { NONE, + NO_ACTION, PLAY_PAUSE, NEXT_TRACK, PREVIOUS_TRACK, diff --git a/app/src/main/java/eu/darken/capod/reaction/core/stem/StemActionSettings.kt b/app/src/main/java/eu/darken/capod/reaction/core/stem/StemActionSettings.kt index 97ed17f9..da76dbbd 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/stem/StemActionSettings.kt +++ b/app/src/main/java/eu/darken/capod/reaction/core/stem/StemActionSettings.kt @@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.stem import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.datastore.createValue @@ -29,4 +30,8 @@ class StemActionSettings @Inject constructor( val rightDouble = dataStore.createValue("stem.right.double", StemAction.NONE, json, onErrorFallbackToDefault = true) val rightTriple = dataStore.createValue("stem.right.triple", StemAction.NONE, json, onErrorFallbackToDefault = true) val rightLong = dataStore.createValue("stem.right.long", StemAction.NONE, json, onErrorFallbackToDefault = true) + + suspend fun resetAll() { + context.dataStore.edit { it.clear() } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 345afb37..4f954f70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -510,11 +510,14 @@ Long Press Left Right - None + Default + No Action Play/Pause Next Track Previous Track Volume Up Volume Down + Reset to defaults + Reset all stem actions to defaults? \ No newline at end of file