fix(settings): Show passive info when device is connected but AAP unavailable

This commit is contained in:
darken
2026-04-14 11:28:41 +02:00
committed by Matthias Urhahn
parent 65a74e9bc1
commit a37b9b0213
5 changed files with 67 additions and 8 deletions
@@ -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(
@@ -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()
}
+2
View File
@@ -437,6 +437,8 @@
<string name="device_settings_not_connected_description">This device is nearby but not connected to this phone. Connect to access settings and controls.</string>
<string name="device_settings_not_connected_connect_action">Connect</string>
<string name="device_settings_not_connected_open_settings_action">Open Bluetooth Settings</string>
<string name="device_settings_aap_unavailable_label">Advanced settings unavailable</string>
<string name="device_settings_aap_unavailable_description">This phone\'s Bluetooth does not support the connection required for advanced AirPods settings.</string>
<string name="device_settings_category_sound_label">Sound</string>
<string name="device_settings_category_controls_label">Controls</string>
<string name="device_settings_nc_one_airpod_label">Noise Cancellation with One AirPod</string>
@@ -53,6 +53,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
private lateinit var upgradeInfoFlow: MutableStateFlow<UpgradeRepo.Info>
private lateinit var connectedDevicesFlow: MutableStateFlow<List<BluetoothDevice2>>
private fun mockBondedDevice(address: BluetoothAddress): BluetoothDevice2 = mockk {
every { this@mockk.address } returns address
@@ -80,9 +81,11 @@ class DeviceSettingsViewModelTest : BaseTest() {
upgradeRepo = mockk<UpgradeRepo>().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
}
}
@@ -17,7 +17,6 @@ class AutoConnectLogicTest : BaseTest() {
autoConnect = AutoConnect(
bluetoothManager = mockk(relaxed = true),
deviceMonitor = mockk(relaxed = true),
generalSettings = mockk(relaxed = true),
)
}