diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt index 03b46cb38..5f6532818 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt @@ -1181,7 +1181,16 @@ class AACPManager(private val device: AppleDevice) { val payload = data.command.payload.toByteArray() val timestamp = Clock.System.now() if (payload.size == 18) { - val heartRate = payload[1].toInt() + // unsigned: a signed read turns anything >= 128 bpm negative and drops it + val heartRate = payload[1].toInt() and 0xFF + + // bit 0 of the last byte is set for the first few samples after the sensor starts (payload[2], + // which looks like a confidence value, is also very low then); those readings are unreliable, + // e.g. 169 bpm at rest, so skip them and stay in WAITING until the sensor settles + if (payload[17].toInt() and 0x01 != 0) { + Log.d(TAG, "skipping heart rate sample while the sensor is acquiring: ${payload.toHexString()}") + return + } // same as healthconnect's datatype. 300 isn't possible anyway, but whatever if (heartRate !in 1..300) { @@ -1202,7 +1211,7 @@ class AACPManager(private val device: AppleDevice) { Log.i( TAG, - "hr: $heartRate bpm" + "hr: $heartRate bpm, payload: ${payload.toHexString()}" ) val heartRateSample = HeartRateSample( diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt index 595a791b9..803957cff 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.kavishdevar.librepods.bluetooth.MacAddress @@ -124,8 +125,13 @@ class AppleDevice( } override fun connect(): Boolean { - _connectionState.update { - ConnectionState.CONNECTING + // the service (ACL/UUID broadcasts) and the UI can both call connect(); only let one of them open a socket + val previousState = _connectionState.getAndUpdate { + if (it == ConnectionState.CONNECTING || it == ConnectionState.CONNECTED) it else ConnectionState.CONNECTING + } + if (previousState == ConnectionState.CONNECTING || previousState == ConnectionState.CONNECTED) { + Log.d(TAG, "connect() ignored, already $previousState") + return previousState == ConnectionState.CONNECTED } val success = aacp.connect() // && att.connect() diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt index df87fe626..eb2617b0c 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt @@ -24,6 +24,7 @@ import android.os.BatteryManager import android.os.Binder import android.os.Build import android.os.IBinder +import android.os.Looper import android.os.ParcelUuid import android.os.ext.SdkExtensions import android.provider.Settings @@ -308,6 +309,8 @@ class LibrePodsService: Service() { } } + // createDevice() already registered observers for this device; cancel them so each state change is handled once + deviceJobs[MacAddress(bluetoothDevice.address)]?.forEach { it.cancel() } deviceJobs[MacAddress(bluetoothDevice.address)] = mutableListOf() deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleState(device)) @@ -676,7 +679,9 @@ class LibrePodsService: Service() { Log.d(TAG, "updating island window") if (islandWindow?.isVisible == true) { - islandWindow?.updateBattery(state.battery) + CoroutineScope(Dispatchers.Main).launch { + islandWindow?.updateBattery(state.battery) + } } Log.d(TAG, "updating notification") @@ -919,6 +924,14 @@ class LibrePodsService: Service() { reversed: Boolean = false, otherDeviceName: String? = null ) { + // the island is a window, so it has to be added from the main thread (state observers run on IO) + if (Looper.myLooper() != Looper.getMainLooper()) { + CoroutineScope(Dispatchers.Main).launch { + showIsland(device, type, reversed, otherDeviceName) + } + return + } + Log.d(TAG, "Showing island window") val state = device.state.value @@ -1221,7 +1234,9 @@ class LibrePodsService: Service() { } if (new == EarPresence.NONE && islandWindow?.isVisible == true) { - islandWindow?.close() + CoroutineScope(Dispatchers.Main).launch { + islandWindow?.close() + } } var justEnabledA2dp = false