mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
refactor: Reduce debug log noise from redundant state emissions
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user