Skip to content

Commit 23746d8

Browse files
committed
Fix volume settings applied to speaker when Bluetooth is turned off
When Bluetooth is disabled (via settings, airplane mode, or device reboot), Android doesn't fire ACTION_ACL_DISCONNECTED events for connected devices. This caused the app to continue applying Bluetooth device volume settings to the built-in speaker, potentially resulting in unexpected audio levels. The fix monitors Bluetooth adapter state changes using flatMapLatest on the isEnabled flow. When Bluetooth is turned OFF, pairedDevices now correctly emits only the speaker device, ensuring proper volume settings are applied. Also refactored BluetoothRepo for better readability: - Extracted bluetoothDeviceEvents() function from inline callbackFlow - Moved retryWhen outside flatMapLatest for better state re-evaluation - Simplified speaker device addition to single location - Made State.devices non-nullable for type safety Fixes issue reported via email regarding volume management during BT state changes.
1 parent ad4e375 commit 23746d8

File tree

4 files changed

+36
-27
lines changed

4 files changed

+36
-27
lines changed

app/src/main/java/eu/darken/bluemusic/bluetooth/core/BluetoothRepo.kt

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ import eu.darken.bluemusic.devices.core.DeviceAddr
2727
import kotlinx.coroutines.CoroutineScope
2828
import kotlinx.coroutines.channels.awaitClose
2929
import kotlinx.coroutines.channels.trySendBlocking
30+
import kotlinx.coroutines.currentCoroutineContext
3031
import kotlinx.coroutines.delay
3132
import kotlinx.coroutines.flow.Flow
3233
import kotlinx.coroutines.flow.callbackFlow
3334
import kotlinx.coroutines.flow.combine
3435
import kotlinx.coroutines.flow.distinctUntilChanged
36+
import kotlinx.coroutines.flow.flatMapLatest
3537
import kotlinx.coroutines.flow.flow
38+
import kotlinx.coroutines.flow.flowOf
3639
import kotlinx.coroutines.flow.map
3740
import kotlinx.coroutines.flow.retry
3841
import kotlinx.coroutines.flow.retryWhen
@@ -81,7 +84,7 @@ class BluetoothRepo @Inject constructor(
8184

8285
@SuppressLint("MissingPermission")
8386
@RequiresPermission(anyOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH])
84-
private val pairedDevices: Flow<Set<SourceDevice>?> = callbackFlow {
87+
private fun bluetoothDeviceEvents(): Flow<Unit> = callbackFlow {
8588
if (!isBluetoothSupported) {
8689
log(TAG, WARN) { "Bluetooth hardware is not available" }
8790
throw IllegalStateException("Bluetooth hardware is not available")
@@ -124,35 +127,41 @@ class BluetoothRepo @Inject constructor(
124127
context.unregisterReceiver(receiver)
125128
}
126129
}
127-
.map {
128-
val devices = mutableSetOf<SourceDevice>()
129-
130-
bluetoothAdapter.bondedDevices
131-
.filterNot { device ->
132-
val isHealthDevice = device.hasUUID(0x1400)
133-
if (isHealthDevice) log(TAG) { "Health devices are excluded: ${device.name} - ${device.address}" }
134-
isHealthDevice
135-
}
136-
.forEach { device ->
137-
val wrapper = SourceDeviceWrapper.from(realDevice = device, isConnected = device.isConnected())
138-
devices.add(wrapper)
139-
log(TAG) { "Paired evice: ${wrapper.name} - ${wrapper.address} - isConnected=${wrapper.isConnected}" }
140-
141-
}
142130

143-
devices.add(speakerDeviceProvider.getSpeaker())
131+
@SuppressLint("MissingPermission")
132+
@RequiresPermission(anyOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH])
133+
private val pairedDevices: Flow<Set<SourceDevice>> = isEnabled
134+
.flatMapLatest { enabled ->
135+
if (!enabled) {
136+
log(TAG, WARN) { "Bluetooth is disabled, emitting empty device set" }
137+
return@flatMapLatest flowOf(emptySet())
138+
}
144139

145-
devices.toSet() as Set<SourceDevice>?
140+
bluetoothDeviceEvents().map {
141+
bluetoothAdapter.bondedDevices
142+
.filterNot { device ->
143+
val isHealthDevice = device.hasUUID(0x1400)
144+
if (isHealthDevice) log(TAG) { "Health devices are excluded: ${device.name} - ${device.address}" }
145+
isHealthDevice
146+
}
147+
.map { device ->
148+
val wrapper = SourceDeviceWrapper.from(realDevice = device, isConnected = device.isConnected())
149+
log(TAG) { "Paired device: ${wrapper.name} - ${wrapper.address} - isConnected=${wrapper.isConnected}" }
150+
wrapper as SourceDevice
151+
}
152+
.toSet()
153+
}
146154
}
147155
.retryWhen { err, attempt ->
148156
log(TAG, WARN) { "Can't load paired devices: ${err.asLog()}" }
149-
emit(null)
157+
emit(emptySet<SourceDevice>())
150158
delay(3000)
151159
true
152160
}
161+
.map { btDevices -> btDevices + speakerDeviceProvider.getSpeaker() }
153162

154163
private val hasPermission = flow {
155-
while (coroutineContext.isActive) {
164+
while (currentCoroutineContext().isActive) {
156165
emit(permissionHelper.hasBluetoothPermission())
157166
delay(1000)
158167
}
@@ -161,10 +170,10 @@ class BluetoothRepo @Inject constructor(
161170
data class State(
162171
val isEnabled: Boolean,
163172
val hasPermission: Boolean,
164-
val devices: Set<SourceDevice>?,
173+
val devices: Set<SourceDevice>,
165174
val error: Throwable? = null,
166175
) {
167-
val isReady = isEnabled && hasPermission && devices != null && error == null
176+
val isReady = isEnabled && hasPermission && error == null
168177
}
169178

170179
val state = combine(
@@ -175,7 +184,7 @@ class BluetoothRepo @Inject constructor(
175184
State(
176185
isEnabled = enabled,
177186
hasPermission = permission,
178-
devices = devices ?: emptySet(),
187+
devices = devices,
179188
)
180189
}
181190
.retry {

app/src/main/java/eu/darken/bluemusic/bluetooth/ui/discover/DiscoverViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class DiscoverViewModel @Inject constructor(
3535
upgradeRepo.upgradeInfo,
3636
) { bluetoothState, managed, upgradeInfo ->
3737
State(
38-
devices = bluetoothState.devices!!
38+
devices = bluetoothState.devices
3939
.filterNot { p -> managed.any { p.address == it.address } }
4040
.sortedWith(
4141
compareBy(

app/src/main/java/eu/darken/bluemusic/devices/core/DeviceRepo.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ class DeviceRepo @Inject constructor(
2929
bluetoothRepo.state,
3030
deviceDatabase.devices.getAllDevices()
3131
) { btState, managed ->
32-
val pairedMap = btState.devices?.associateBy { it.address }
32+
val pairedMap = btState.devices.associateBy { it.address }
3333
val mappings = managed
3434
.mapNotNull { config ->
35-
val paired = pairedMap?.get(config.address) ?: return@mapNotNull null
35+
val paired = pairedMap[config.address] ?: return@mapNotNull null
3636
config to paired
3737
}
3838

app/src/main/java/eu/darken/bluemusic/devices/core/NewDeviceCreator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class NewDeviceCreator @Inject constructor(
2626
val device = bluetoothRepo.state
2727
.filter { it.isReady }
2828
.first()
29-
.devices!!
29+
.devices
3030
.find { it.address == address } ?: throw IllegalStateException("Device not found: $address")
3131

3232
var config = DeviceConfigEntity(

0 commit comments

Comments
 (0)