From a37b9b021347aece260c3ba8956ab3fd91803534 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 13 Apr 2026 14:48:24 +0200 Subject: [PATCH] fix(settings): Show passive info when device is connected but AAP unavailable --- .../ui/devicesettings/DeviceSettingsScreen.kt | 43 ++++++++++++++++--- .../devicesettings/DeviceSettingsViewModel.kt | 6 ++- app/src/main/res/values/strings.xml | 2 + .../DeviceSettingsViewModelTest.kt | 23 ++++++++++ .../core/autoconnect/AutoConnectLogicTest.kt | 1 - 5 files changed, 67 insertions(+), 8 deletions(-) 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 6abada80..13fba17c 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 @@ -273,12 +273,20 @@ fun DeviceSettingsScreen( // Not connected info — BLE live but no AAP connection if (device != null && device.ble != null && !device.isAapConnected && device.address != null) { - item("not_connected_info") { - NotConnectedCard( - isNudgeAvailable = state.isNudgeAvailable, - isForceConnecting = state.isForceConnecting, - onConnect = onForceConnect, - ) + if (state.isClassicallyConnected) { + // Device is connected for audio but AAP isn't available — show passive info + item("aap_unavailable_info") { + AapUnavailableCard() + } + } else { + // Device is nearby but not connected — prompt user to connect + item("not_connected_info") { + NotConnectedCard( + isNudgeAvailable = state.isNudgeAvailable, + isForceConnecting = state.isForceConnecting, + onConnect = onForceConnect, + ) + } } } @@ -827,6 +835,29 @@ private fun NotConnectedCard( } } +@Composable +private fun AapUnavailableCard() { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.device_settings_aap_unavailable_label), + style = MaterialTheme.typography.titleMedium, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.device_settings_aap_unavailable_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + @Composable private fun InfoRow(label: String, value: String, modifier: Modifier = Modifier) { Column( 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 9677eb7c..0511c161 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 @@ -80,13 +80,16 @@ class DeviceSettingsViewModel @Inject constructor( deviceForProfile(profileId), upgradeRepo.upgradeInfo, isForceConnecting, - ) { _, device, upgrade, forcing -> + bluetoothManager.connectedDevices, + ) { _, device, upgrade, forcing, connectedDevices -> + val connectedAddresses = connectedDevices.map { it.address }.toSet() State( device = device, now = Instant.now(), isPro = upgrade.isPro, isNudgeAvailable = bluetoothManager.isNudgeAvailable, isForceConnecting = forcing, + isClassicallyConnected = device?.address?.let { it in connectedAddresses } == true, ) } }.asLiveState() @@ -108,6 +111,7 @@ class DeviceSettingsViewModel @Inject constructor( val isPro: Boolean = false, val isNudgeAvailable: Boolean = true, val isForceConnecting: Boolean = false, + val isClassicallyConnected: Boolean = false, ) { val reactions: ReactionConfig get() = device?.reactions ?: ReactionConfig() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ed64fa2..af09f293 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -437,6 +437,8 @@ This device is nearby but not connected to this phone. Connect to access settings and controls. Connect Open Bluetooth Settings + Advanced settings unavailable + This phone\'s Bluetooth does not support the connection required for advanced AirPods settings. Sound Controls Noise Cancellation with One AirPod diff --git a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt index ca8bbf88..2df3ce66 100644 --- a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt +++ b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt @@ -53,6 +53,7 @@ class DeviceSettingsViewModelTest : BaseTest() { private lateinit var devicesFlow: MutableStateFlow> private lateinit var upgradeInfoFlow: MutableStateFlow + private lateinit var connectedDevicesFlow: MutableStateFlow> private fun mockBondedDevice(address: BluetoothAddress): BluetoothDevice2 = mockk { every { this@mockk.address } returns address @@ -80,9 +81,11 @@ class DeviceSettingsViewModelTest : BaseTest() { upgradeRepo = mockk().also { every { it.upgradeInfo } returns upgradeInfoFlow } + connectedDevicesFlow = MutableStateFlow(emptyList()) bluetoothManager = mockk(relaxed = true) { every { isNudgeAvailable } returns true every { bondedDevices() } returns flowOf(emptySet()) + every { connectedDevices } returns connectedDevicesFlow } profilesRepo = mockk(relaxed = true) generalSettings = mockk(relaxed = true) @@ -275,4 +278,24 @@ class DeviceSettingsViewModelTest : BaseTest() { sendFailed.command shouldBe AapCommand.SetDeviceName("NewName") sendFailed.message shouldBe "socket closed" } + + @Test + fun `isClassicallyConnected is true when device address is in connected devices`() = runTest(testDispatcher) { + connectedDevicesFlow.value = listOf(mockBondedDevice(testAddress)) + + val vm = createViewModel() + vm.initialize(testAddress) + + vm.state.first().isClassicallyConnected shouldBe true + } + + @Test + fun `isClassicallyConnected is false when device address is not in connected devices`() = runTest(testDispatcher) { + connectedDevicesFlow.value = listOf(mockBondedDevice("XX:XX:XX:XX:XX:XX")) + + val vm = createViewModel() + vm.initialize(testAddress) + + vm.state.first().isClassicallyConnected shouldBe false + } } diff --git a/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt b/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt index ad43f9ed..fc6de09c 100644 --- a/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt +++ b/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt @@ -17,7 +17,6 @@ class AutoConnectLogicTest : BaseTest() { autoConnect = AutoConnect( bluetoothManager = mockk(relaxed = true), deviceMonitor = mockk(relaxed = true), - generalSettings = mockk(relaxed = true), ) }