diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index b267c523..a3d1682b 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -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), diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt index ca39fb12..1adc2282 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt index 4a444fe6..ca4e3005 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt @@ -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