refactor: Reduce debug log noise from redundant state emissions

This commit is contained in:
darken
2026-04-25 16:19:21 +02:00
committed by Matthias Urhahn
parent c2ff1f111e
commit 486efe6e65
3 changed files with 216 additions and 24 deletions
@@ -11,6 +11,7 @@ import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
@@ -35,6 +36,7 @@ import eu.darken.capod.monitor.core.primaryDevice
import eu.darken.capod.monitor.ui.MonitorNotifications
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.profiles.core.DeviceProfile
import eu.darken.capod.profiles.core.DeviceProfilesRepo
import eu.darken.capod.reaction.core.autoconnect.AutoConnect
@@ -49,6 +51,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
@@ -225,31 +228,19 @@ class MonitorService : Service() {
profilesRepo.profiles,
bluetoothManager.connectedDevices,
aapConnectionManager.allStates,
) { monitorMode, profiles, connectedDevices, aapStates ->
listOf(monitorMode, profiles, connectedDevices, aapStates)
) { mode, profiles, devices, aapStates ->
buildMonitorModeState(mode, profiles, devices, aapStates)
}
}
}
.distinctUntilChanged()
.setupCommonEventHandlers(TAG) { "MonitorMode" }
.flatMapLatest { arguments ->
val monitorMode = arguments[0] as MonitorMode
.flatMapLatest { state ->
log(TAG) { "Monitor mode: ${state.mode}" }
log(TAG) { "connectedAddresses: ${state.connectedAddresses}" }
log(TAG) { "knownAddresses: ${state.knownAddresses}" }
@Suppress("UNCHECKED_CAST")
val profiles = arguments[1] as List<DeviceProfile>
@Suppress("UNCHECKED_CAST")
val devices = arguments[2] as Collection<BluetoothDevice2>
@Suppress("UNCHECKED_CAST")
val aapStates = arguments[3] as Map<*, *>
val connectedAddresses = devices.map { it.address }.toSet()
val knownAddresses = profiles.mapNotNull { it.address }.toSet()
log(TAG) { "Monitor mode: $monitorMode" }
log(TAG) { "connectedAddresses: $connectedAddresses" }
log(TAG) { "knownAddresses: $knownAddresses" }
when (monitorMode) {
when (state.mode) {
MonitorMode.MANUAL -> flow<Unit> {
monitorScope.coroutineContext.cancelChildren()
}
@@ -257,11 +248,11 @@ class MonitorService : Service() {
MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow {
when {
profiles.isEmpty() && devices.isNotEmpty() -> {
!state.hasProfiles && state.connectedAddresses.isNotEmpty() -> {
log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
}
knownAddresses.any { it in connectedAddresses } || aapStates.isNotEmpty() -> {
state.knownAddresses.any { it in state.connectedAddresses } || state.hasAapSession -> {
log(TAG) { "A device is connected, aborting any timeout." }
}
@@ -347,6 +338,27 @@ class MonitorService : Service() {
}
}
internal data class MonitorModeState(
val mode: MonitorMode,
val hasProfiles: Boolean,
val knownAddresses: Set<BluetoothAddress>,
val connectedAddresses: Set<BluetoothAddress>,
val hasAapSession: Boolean,
)
internal fun buildMonitorModeState(
mode: MonitorMode,
profiles: List<DeviceProfile>,
devices: Collection<BluetoothDevice2>,
aapStates: Map<BluetoothAddress, AapPodState>,
): MonitorModeState = MonitorModeState(
mode = mode,
hasProfiles = profiles.isNotEmpty(),
knownAddresses = profiles.mapNotNull { it.address }.toSet(),
connectedAddresses = devices.map { it.address }.toSet(),
hasAapSession = aapStates.isNotEmpty(),
)
private data class NotificationDeviceKey(
val profileId: String?,
val label: String?,
@@ -3,7 +3,6 @@ package eu.darken.capod.pods.core.apple.ble
import dagger.Reusable
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.logSummary
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
@@ -26,7 +25,7 @@ class PodFactory @Inject constructor(
device = unknownFactory.create(scanResult)
}
log(TAG, DEBUG) { "Pod created: ${device.logSummary()}" }
log(TAG, VERBOSE) { "Pod created: ${device.logSummary()}" }
return Result(scanResult = scanResult, device = device)
}
@@ -0,0 +1,181 @@
package eu.darken.capod.monitor.core.worker
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.main.core.MonitorMode
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.profiles.core.AppleDeviceProfile
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import java.time.Instant
class MonitorModeStateTest : BaseTest() {
private val addressA: BluetoothAddress = "AA:BB:CC:DD:EE:01"
private val addressB: BluetoothAddress = "AA:BB:CC:DD:EE:02"
private val profileA = AppleDeviceProfile(
label = "AirPods A",
model = PodModel.AIRPODS_PRO2_USBC,
address = addressA,
)
private val profileB = AppleDeviceProfile(
label = "AirPods B",
model = PodModel.AIRPODS_PRO2_USBC,
address = addressB,
)
private val noAddressProfile = AppleDeviceProfile(
label = "Profile no address",
model = PodModel.AIRPODS_PRO2_USBC,
address = null,
)
private val aapStateA = AapPodState(
connectionState = AapPodState.ConnectionState.READY,
lastMessageAt = Instant.parse("2026-04-25T12:00:00Z"),
)
private fun mockDevice(addr: BluetoothAddress): BluetoothDevice2 = mockk {
every { address } returns addr
}
@Test
fun `empty profiles - hasProfiles false and addresses empty`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = emptyList(),
devices = emptyList(),
aapStates = emptyMap(),
)
state.hasProfiles shouldBe false
state.knownAddresses shouldBe emptySet()
state.connectedAddresses shouldBe emptySet()
state.hasAapSession shouldBe false
state.mode shouldBe MonitorMode.AUTOMATIC
}
@Test
fun `profile with valid address - hasProfiles true and address present`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileA),
devices = emptyList(),
aapStates = emptyMap(),
)
state.hasProfiles shouldBe true
state.knownAddresses shouldBe setOf(addressA)
}
@Test
fun `profile with null address - hasProfiles true but knownAddresses empty`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(noAddressProfile),
devices = emptyList(),
aapStates = emptyMap(),
)
state.hasProfiles shouldBe true
state.knownAddresses shouldBe emptySet()
}
@Test
fun `mixed profiles - hasProfiles true and only addressed profile contributes`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileA, noAddressProfile),
devices = emptyList(),
aapStates = emptyMap(),
)
state.hasProfiles shouldBe true
state.knownAddresses shouldBe setOf(addressA)
}
@Test
fun `connectedDevices addresses derived from BluetoothDevice2 address`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = emptyList(),
devices = listOf(mockDevice(addressA), mockDevice(addressB)),
aapStates = emptyMap(),
)
state.connectedAddresses shouldBe setOf(addressA, addressB)
}
@Test
fun `aapStates non-empty - hasAapSession true`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = emptyList(),
devices = emptyList(),
aapStates = mapOf(addressA to aapStateA),
)
state.hasAapSession shouldBe true
}
@Test
fun `aapStates empty - hasAapSession false`() {
val state = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = emptyList(),
devices = emptyList(),
aapStates = emptyMap(),
)
state.hasAapSession shouldBe false
}
@Test
fun `equal inputs produce equal states - data class equality enables distinctUntilChanged`() {
val first = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileA, profileB),
devices = listOf(mockDevice(addressA)),
aapStates = mapOf(addressA to aapStateA),
)
val second = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileA, profileB),
devices = listOf(mockDevice(addressA)),
aapStates = mapOf(addressA to aapStateA),
)
first shouldBe second
}
@Test
fun `reordered profile list with same addresses produces equal knownAddresses`() {
val first = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileA, profileB),
devices = emptyList(),
aapStates = emptyMap(),
)
val second = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = listOf(profileB, profileA),
devices = emptyList(),
aapStates = emptyMap(),
)
first.knownAddresses shouldBe second.knownAddresses
first shouldBe second
}
@Test
fun `mode change produces unequal states`() {
val auto = buildMonitorModeState(
mode = MonitorMode.AUTOMATIC,
profiles = emptyList(),
devices = emptyList(),
aapStates = emptyMap(),
)
val manual = buildMonitorModeState(
mode = MonitorMode.MANUAL,
profiles = emptyList(),
devices = emptyList(),
aapStates = emptyMap(),
)
(auto == manual) shouldBe false
}
}