fix(settings): Fix OFF visibility toggle not reacting on first press

Serialize AapConnection.send() with a dedicated sendMutex so two concurrent optimistic state updates cannot clobber each other. Collapse the OFF toggle click into a single combined VM call that runs both SetListeningModeCycle and SetAllowOffOption sequentially.
This commit is contained in:
darken
2026-04-14 11:28:41 +02:00
committed by Matthias Urhahn
parent 1f7bc7cfd8
commit 2a8416d146
3 changed files with 28 additions and 8 deletions
@@ -155,6 +155,7 @@ fun DeviceSettingsScreenHost(
onMicrophoneModeChange = { vm.setMicrophoneMode(it) },
onListeningModeCycleChange = { vm.setListeningModeCycle(it) },
onAllowOffOptionChange = { vm.setAllowOffOption(it) },
onOffVisibilityChange = { enabled, mask -> vm.setListeningModeOffVisibility(enabled, mask) },
onSleepDetectionChange = { vm.setSleepDetection(it) },
onDeviceNameChange = { vm.setDeviceName(it) },
onStemActionsClick = { vm.navToStemConfig() },
@@ -190,6 +191,7 @@ fun DeviceSettingsScreen(
onMicrophoneModeChange: (AapSetting.MicrophoneMode.Mode) -> Unit = {},
onListeningModeCycleChange: (Int) -> Unit = {},
onAllowOffOptionChange: (Boolean) -> Unit = {},
onOffVisibilityChange: (enabled: Boolean, currentCycleMask: Int) -> Unit = { _, _ -> },
onSleepDetectionChange: (Boolean) -> Unit = {},
onDeviceNameChange: (String) -> Unit = {},
onStemActionsClick: () -> Unit = {},
@@ -402,6 +404,7 @@ fun DeviceSettingsScreen(
cycleMask = if (isPro) cycleMask else null,
onCycleMaskChange = onListeningModeCycleChange,
onAllowOffChange = onAllowOffOptionChange,
onOffVisibilityChange = onOffVisibilityChange,
enabled = enabled,
)
}
@@ -1253,6 +1256,7 @@ private fun NoiseControlCombined(
cycleMask: Int?,
onCycleMaskChange: (Int) -> Unit,
onAllowOffChange: (Boolean) -> Unit = {},
onOffVisibilityChange: (enabled: Boolean, currentCycleMask: Int) -> Unit = { _, _ -> },
enabled: Boolean,
) {
val displayMode = pendingMode ?: currentMode
@@ -1288,12 +1292,12 @@ private fun NoiseControlCombined(
IconButton(
onClick = {
val isOff = mode == AapSetting.AncMode.Value.OFF
if (inCycle == true && canRemoveFromCycle) {
if (isOff) {
onOffVisibilityChange(inCycle != true, cycleMask ?: 0)
} else if (inCycle == true && canRemoveFromCycle) {
onCycleMaskChange(cycleMask xor bit)
if (isOff) onAllowOffChange(false)
} else if (inCycle != true) {
onCycleMaskChange((cycleMask ?: 0) or bit)
if (isOff) onAllowOffChange(true)
}
},
enabled = enabled && (inCycle != true || canRemoveFromCycle),
@@ -167,14 +167,16 @@ class DeviceSettingsViewModel @Inject constructor(
}
}
private suspend fun sendInternal(command: AapCommand) {
val address = currentAddress() ?: return
try {
private suspend fun sendInternal(command: AapCommand): Boolean {
val address = currentAddress() ?: return false
return try {
aapManager.sendCommand(address, command)
log(TAG) { "Sent $command to $address" }
true
} catch (e: Exception) {
log(TAG, WARN) { "Failed to send $command: ${e.message}" }
events.emit(Event.SendFailed(command, e.message))
false
}
}
@@ -223,6 +225,19 @@ class DeviceSettingsViewModel @Inject constructor(
fun setAllowOffOption(enabled: Boolean) = sendProGated(AapCommand.SetAllowOffOption(enabled))
fun setListeningModeOffVisibility(enabled: Boolean, currentCycleMask: Int) = launch {
if (!upgradeRepo.isPro()) {
navTo(Nav.Main.Upgrade)
return@launch
}
// Keep in sync with cycleBits[OFF] in DeviceSettingsScreen.NoiseControlCombined
val offBit = 0x01
val newMask = if (enabled) currentCycleMask or offBit else currentCycleMask and offBit.inv()
if (sendInternal(AapCommand.SetListeningModeCycle(newMask))) {
sendInternal(AapCommand.SetAllowOffOption(enabled))
}
}
fun setSleepDetection(enabled: Boolean) = sendProGated(AapCommand.SetSleepDetection(enabled))
fun setDeviceName(name: String) = launch {
@@ -65,6 +65,7 @@ internal class AapConnection(
private var socket: BluetoothSocket? = null
private var readerJob: Job? = null
private val writeMutex = Mutex()
private val sendMutex = Mutex()
private val framer = AapFramer()
private var pendingAncMode: AapSetting.AncMode.Value? = null
@@ -147,7 +148,7 @@ internal class AapConnection(
_state.value = AapPodState(connectionState = AapPodState.ConnectionState.DISCONNECTED)
}
suspend fun send(command: AapCommand) {
suspend fun send(command: AapCommand) = sendMutex.withLock {
val currentState = _state.value
if (currentState.connectionState != AapPodState.ConnectionState.READY) {
throw IllegalStateException("Cannot send command in state ${currentState.connectionState}")
@@ -162,7 +163,7 @@ internal class AapConnection(
log(TAG) { "No pod in ear, queuing ANC mode: ${command.mode}" }
pendingAncMode = command.mode
_state.value = currentState.copy(pendingAncMode = command.mode)
return
return@withLock
}
pendingAncMode = null
// Optimistically update UI — don't wait for device echo