mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix(device-settings): Fix ANC OFF mode visibility and inference logic
Fix Elvis operator precedence bug in visibleAncModes() where OFF passed the filter unconditionally. Move filtering logic to PodDevice.visibleAncModes extension, unifying DualPodsCard, SinglePodsCard, and DeviceSettingsScreen. Gate AllowOffOption inference on pod-in-ear + 1.5s stability to prevent false positives from in-case OFF reports.
This commit is contained in:
@@ -28,19 +28,3 @@ fun AapSetting.AncMode.Value.icon(): ImageVector = when (this) {
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> Icons.TwoTone.AutoAwesome
|
||||
}
|
||||
|
||||
fun AapSetting.AncMode.Value.cycleBit(): Int = when (this) {
|
||||
AapSetting.AncMode.Value.OFF -> 0x01
|
||||
AapSetting.AncMode.Value.ON -> 0x02
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> 0x04
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> 0x08
|
||||
}
|
||||
|
||||
fun visibleAncModes(
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
currentMode: AapSetting.AncMode.Value,
|
||||
cycleMask: Int?,
|
||||
allowOffEnabled: Boolean,
|
||||
): List<AapSetting.AncMode.Value> = supportedModes.filter { mode ->
|
||||
val inCycle = cycleMask?.let { (it and mode.cycleBit()) != 0 } ?: mode != AapSetting.AncMode.Value.OFF
|
||||
inCycle || (mode == AapSetting.AncMode.Value.OFF && allowOffEnabled) || mode == currentMode
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ import eu.darken.capod.main.ui.devicesettings.dialogs.SystemRenameUnavailableDia
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||
import eu.darken.capod.monitor.core.lastSeenFormatted
|
||||
import eu.darken.capod.monitor.core.resolvedAncCycleMask
|
||||
import eu.darken.capod.monitor.core.visibleAncModes
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
@@ -516,9 +518,7 @@ fun DeviceSettingsScreen(
|
||||
val ancMode = device.ancMode
|
||||
val adaptiveNoise = device.adaptiveAudioNoise
|
||||
if (features.hasAncControl && ancMode != null) {
|
||||
val cycleMask = if (features.hasListeningModeCycle) {
|
||||
(device.listeningModeCycle ?: AapSetting.ListeningModeCycle(modeMask = 0x0E)).modeMask
|
||||
} else null
|
||||
val cycleMask = device.resolvedAncCycleMask
|
||||
val cycleSummary = if (cycleMask != null) listeningModeCycleSummary(context, ancMode.supported, cycleMask) else null
|
||||
val cycleSubtitle = if (cycleSummary != null) {
|
||||
buildString {
|
||||
@@ -543,7 +543,7 @@ fun DeviceSettingsScreen(
|
||||
NoiseControlCurrentModeControl(
|
||||
currentMode = ancMode.current,
|
||||
pendingMode = device.pendingAncMode,
|
||||
supportedModes = ancMode.supported,
|
||||
supportedModes = device.visibleAncModes,
|
||||
onModeSelected = onAncModeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.main.ui.components.visibleAncModes
|
||||
import eu.darken.capod.monitor.core.visibleAncModes
|
||||
import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector
|
||||
import eu.darken.capod.main.ui.overview.cards.components.BatteryCapsule
|
||||
import eu.darken.capod.main.ui.overview.cards.components.DebugSection
|
||||
@@ -237,15 +237,9 @@ fun DualPodsCard(
|
||||
val ancMode = device.ancMode
|
||||
if (device.isAapConnected && device.hasAncControl && ancMode != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val visibleModes = visibleAncModes(
|
||||
supportedModes = ancMode.supported,
|
||||
currentMode = ancMode.current,
|
||||
cycleMask = device.listeningModeCycle?.modeMask ?: 0x0E,
|
||||
allowOffEnabled = device.allowOffOption?.enabled == true,
|
||||
)
|
||||
AncModeSelector(
|
||||
currentMode = ancMode.current,
|
||||
supportedModes = visibleModes,
|
||||
supportedModes = device.visibleAncModes,
|
||||
onModeSelected = { onAncModeChange?.invoke(it) },
|
||||
pendingMode = device.pendingAncMode,
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.main.ui.components.visibleAncModes
|
||||
import eu.darken.capod.monitor.core.visibleAncModes
|
||||
import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector
|
||||
import eu.darken.capod.main.ui.overview.cards.components.DebugSection
|
||||
import eu.darken.capod.main.ui.overview.cards.components.DeviceConnectionBadge
|
||||
@@ -260,15 +260,9 @@ fun SinglePodsCard(
|
||||
val ancMode = device.ancMode
|
||||
if (device.isAapConnected && device.hasAncControl && ancMode != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
val visibleModes = visibleAncModes(
|
||||
supportedModes = ancMode.supported,
|
||||
currentMode = ancMode.current,
|
||||
cycleMask = device.listeningModeCycle?.modeMask ?: 0x0E,
|
||||
allowOffEnabled = device.allowOffOption?.enabled == true,
|
||||
)
|
||||
AncModeSelector(
|
||||
currentMode = ancMode.current,
|
||||
supportedModes = visibleModes,
|
||||
supportedModes = device.visibleAncModes,
|
||||
onModeSelected = { onAncModeChange?.invoke(it) },
|
||||
pendingMode = device.pendingAncMode,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
|
||||
private fun AapSetting.AncMode.Value.cycleBit(): Int = when (this) {
|
||||
AapSetting.AncMode.Value.OFF -> 0x01
|
||||
AapSetting.AncMode.Value.ON -> 0x02
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> 0x04
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> 0x08
|
||||
}
|
||||
|
||||
fun resolvedAncCycleMask(
|
||||
hasListeningModeCycle: Boolean,
|
||||
reportedCycleMask: Int?,
|
||||
): Int? = if (hasListeningModeCycle) {
|
||||
reportedCycleMask ?: 0x0E
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun visibleAncModes(
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
currentMode: AapSetting.AncMode.Value,
|
||||
cycleMask: Int?,
|
||||
allowOffEnabled: Boolean,
|
||||
): List<AapSetting.AncMode.Value> = supportedModes.filter { mode ->
|
||||
val inCycle = if (cycleMask != null) {
|
||||
(cycleMask and mode.cycleBit()) != 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
inCycle || (mode == AapSetting.AncMode.Value.OFF && allowOffEnabled) || mode == currentMode
|
||||
}
|
||||
|
||||
val PodDevice.resolvedAncCycleMask: Int?
|
||||
get() = resolvedAncCycleMask(
|
||||
hasListeningModeCycle = model.features.hasListeningModeCycle,
|
||||
reportedCycleMask = listeningModeCycle?.modeMask,
|
||||
)
|
||||
|
||||
val PodDevice.visibleAncModes: List<AapSetting.AncMode.Value>
|
||||
get() {
|
||||
val ancMode = ancMode ?: return emptyList()
|
||||
return visibleAncModes(
|
||||
supportedModes = ancMode.supported,
|
||||
currentMode = ancMode.current,
|
||||
cycleMask = resolvedAncCycleMask,
|
||||
allowOffEnabled = allowOffOption?.enabled == true,
|
||||
)
|
||||
}
|
||||
@@ -54,11 +54,13 @@ internal class AapSessionEngine(
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private var ancDebounceJob: Job? = null
|
||||
private var allowOffInferenceJob: Job? = null
|
||||
private var lastSentCommand: AapCommand? = null
|
||||
private var lastSentAt: Long = 0L
|
||||
/** Separate tracking for ANC sends — not overwritten by non-ANC commands during flush. */
|
||||
private var lastAncSentAt: Long = 0L
|
||||
private var handshakeResponseReceived: Boolean = false
|
||||
private var latestObservedAncMode: AapSetting.AncMode? = null
|
||||
|
||||
/** Stored reference to the socket write callback — set on each [send] / flush call. */
|
||||
private var activeSendRaw: (suspend (AapCommand) -> Unit)? = null
|
||||
@@ -81,6 +83,7 @@ internal class AapSessionEngine(
|
||||
fun reset() {
|
||||
ancDebounceJob?.cancel()
|
||||
ancDebounceJob = null
|
||||
cancelAllowOffOptionInference()
|
||||
coordinator.clear()
|
||||
hidTracker.flush()
|
||||
hidTracker.reset()
|
||||
@@ -88,6 +91,7 @@ internal class AapSessionEngine(
|
||||
lastSentCommand = null
|
||||
lastSentAt = 0L
|
||||
lastAncSentAt = 0L
|
||||
latestObservedAncMode = null
|
||||
activeSendRaw = null
|
||||
handshakeResponseReceived = false
|
||||
_state.value = AapPodState(connectionState = AapPodState.ConnectionState.DISCONNECTED)
|
||||
@@ -163,6 +167,7 @@ internal class AapSessionEngine(
|
||||
if (outcome is AapSettingsCoordinator.VerificationOutcome.Rejected) {
|
||||
val command = outcome.command
|
||||
if (command is AapCommand.SetAncMode && command.mode == AapSetting.AncMode.Value.OFF) {
|
||||
cancelAllowOffOptionInference()
|
||||
val prev = _state.value.setting<AapSetting.AllowOffOption>()
|
||||
if (prev == null || prev.enabled) {
|
||||
_state.value = _state.value.withSetting(
|
||||
@@ -273,20 +278,20 @@ internal class AapSessionEngine(
|
||||
|
||||
// ANC debounce
|
||||
if (value is AapSetting.AncMode) {
|
||||
latestObservedAncMode = value
|
||||
syncAllowOffOptionInference()
|
||||
val isFirstAncMode = _state.value.setting<AapSetting.AncMode>() == null
|
||||
val recentAncSend = lastAncSentAt > 0L && (timeSource.currentTimeMillis() - lastAncSentAt) <= 3000L
|
||||
if (isFirstAncMode || recentAncSend) {
|
||||
ancDebounceJob?.cancel()
|
||||
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
|
||||
log(TAG) { "Setting: ${key.simpleName} = $value [was: $previous]" }
|
||||
applyInferences(value)
|
||||
} else {
|
||||
ancDebounceJob?.cancel()
|
||||
ancDebounceJob = scope?.launch {
|
||||
delay(1500L)
|
||||
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
|
||||
log(TAG) { "Setting (debounced): ${key.simpleName} = $value [was: $previous]" }
|
||||
applyInferences(value)
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -304,7 +309,7 @@ internal class AapSessionEngine(
|
||||
}
|
||||
_state.value = newState
|
||||
log(TAG) { "Setting: ${key.simpleName} = $value${if (clearPrimaryPod) " (swap, PrimaryPod cleared)" else ""} [was: $previous]" }
|
||||
applyInferences(value)
|
||||
if (value is AapSetting.EarDetection) syncAllowOffOptionInference()
|
||||
|
||||
// Flush queued commands when pod goes in ear
|
||||
if (value is AapSetting.EarDetection && value.isEitherPodInEar) {
|
||||
@@ -422,20 +427,42 @@ internal class AapSessionEngine(
|
||||
|
||||
// ── Inference ───────────────────────────────────────────
|
||||
|
||||
private fun applyInferences(trigger: AapSetting) {
|
||||
val inferred = mutableListOf<Pair<KClass<out AapSetting>, AapSetting>>()
|
||||
private fun syncAllowOffOptionInference() {
|
||||
cancelAllowOffOptionInference()
|
||||
|
||||
if (trigger is AapSetting.AncMode && trigger.current == AapSetting.AncMode.Value.OFF) {
|
||||
val current = _state.value.setting<AapSetting.AllowOffOption>()
|
||||
if (current == null || !current.enabled) {
|
||||
inferred += AapSetting.AllowOffOption::class to AapSetting.AllowOffOption(enabled = true)
|
||||
val observedAncMode = latestObservedAncMode ?: _state.value.setting<AapSetting.AncMode>() ?: return
|
||||
val earDetection = _state.value.setting<AapSetting.EarDetection>() ?: return
|
||||
val allowOffOption = _state.value.setting<AapSetting.AllowOffOption>()
|
||||
if (observedAncMode.current != AapSetting.AncMode.Value.OFF) return
|
||||
if (!earDetection.isEitherPodInEar) return
|
||||
if (allowOffOption?.enabled == true) return
|
||||
|
||||
val inferenceScope = scope ?: return
|
||||
allowOffInferenceJob = inferenceScope.launch {
|
||||
delay(1500L)
|
||||
|
||||
val latestAncMode = latestObservedAncMode ?: _state.value.setting<AapSetting.AncMode>()
|
||||
val latestEarDetection = _state.value.setting<AapSetting.EarDetection>()
|
||||
val latestAllowOffOption = _state.value.setting<AapSetting.AllowOffOption>()
|
||||
if (latestAncMode?.current == AapSetting.AncMode.Value.OFF &&
|
||||
latestEarDetection?.isEitherPodInEar == true &&
|
||||
latestAllowOffOption?.enabled != true
|
||||
) {
|
||||
_state.value = _state.value.withSetting(
|
||||
AapSetting.AllowOffOption::class,
|
||||
AapSetting.AllowOffOption(enabled = true),
|
||||
)
|
||||
log(TAG) {
|
||||
"Inferred: AllowOffOption = AllowOffOption(enabled=true) (from stable in-ear AncMode=OFF)"
|
||||
}
|
||||
}
|
||||
allowOffInferenceJob = null
|
||||
}
|
||||
}
|
||||
|
||||
for ((key, value) in inferred) {
|
||||
_state.value = _state.value.withSetting(key, value)
|
||||
log(TAG) { "Inferred: ${key.simpleName} = $value (from ${trigger::class.simpleName})" }
|
||||
}
|
||||
private fun cancelAllowOffOptionInference() {
|
||||
allowOffInferenceJob?.cancel()
|
||||
allowOffInferenceJob = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class PodDeviceAncModeTest : BaseTest() {
|
||||
|
||||
private val allModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `cycle mask hides OFF when OFF is not allowed`() {
|
||||
visibleAncModes(
|
||||
supportedModes = allModes,
|
||||
currentMode = AapSetting.AncMode.Value.ON,
|
||||
cycleMask = 0x0E,
|
||||
allowOffEnabled = false,
|
||||
) shouldContainExactly listOf(
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `allow off keeps OFF visible even when cycle mask excludes it`() {
|
||||
visibleAncModes(
|
||||
supportedModes = allModes,
|
||||
currentMode = AapSetting.AncMode.Value.ON,
|
||||
cycleMask = 0x0E,
|
||||
allowOffEnabled = true,
|
||||
) shouldContainExactly allModes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `current OFF stays visible even when OFF is otherwise hidden`() {
|
||||
visibleAncModes(
|
||||
supportedModes = allModes,
|
||||
currentMode = AapSetting.AncMode.Value.OFF,
|
||||
cycleMask = 0x0E,
|
||||
allowOffEnabled = false,
|
||||
) shouldContainExactly allModes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null cycle mask shows all supported modes`() {
|
||||
visibleAncModes(
|
||||
supportedModes = allModes,
|
||||
currentMode = AapSetting.AncMode.Value.ON,
|
||||
cycleMask = null,
|
||||
allowOffEnabled = false,
|
||||
) shouldContainExactly allModes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cycle mask with OFF bit set includes OFF`() {
|
||||
visibleAncModes(
|
||||
supportedModes = allModes,
|
||||
currentMode = AapSetting.AncMode.Value.ON,
|
||||
cycleMask = 0x0F,
|
||||
allowOffEnabled = false,
|
||||
) shouldContainExactly allModes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `models with listening mode cycle fall back to default mask when setting is absent`() {
|
||||
resolvedAncCycleMask(
|
||||
hasListeningModeCycle = true,
|
||||
reportedCycleMask = null,
|
||||
) shouldBe 0x0E
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `models without listening mode cycle keep cycle mask null`() {
|
||||
resolvedAncCycleMask(
|
||||
hasListeningModeCycle = false,
|
||||
reportedCycleMask = null,
|
||||
) shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reported cycle mask wins over fallback`() {
|
||||
resolvedAncCycleMask(
|
||||
hasListeningModeCycle = true,
|
||||
reportedCycleMask = 0x0A,
|
||||
) shouldBe 0x0A
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,10 @@ class AapSessionEngineTest : BaseTest() {
|
||||
return AapMessage(raw = raw, commandType = commandType, payload = ByteArray(0))
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun settingPair(setting: AapSetting): Pair<KClass<out AapSetting>, AapSetting> =
|
||||
setting::class as KClass<out AapSetting> to setting
|
||||
|
||||
/** Build a profile mock with all decode methods stubbed. Does NOT mock encodeCommand (sealed class). */
|
||||
private fun mockProfile(block: AapDeviceProfile.() -> Unit = {}): AapDeviceProfile = mockk {
|
||||
every { decodeStemPress(any()) } returns null
|
||||
@@ -356,26 +360,159 @@ class AapSessionEngineTest : BaseTest() {
|
||||
inner class InferenceTests {
|
||||
|
||||
@Test
|
||||
fun `AncMode OFF infers AllowOffOption true`() {
|
||||
fun `startup OFF while no pod is in ear does not infer AllowOffOption true`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeStemPress(any()) } returns null
|
||||
every { decodeBattery(any()) } returns null
|
||||
every { decodePrivateKeyResponse(any()) } returns null
|
||||
every { decodeDeviceInfo(any()) } returns null
|
||||
every { decodeSetting(any()) } returns (AapSetting.AncMode::class to AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = listOf(AapSetting.AncMode.Value.OFF, AapSetting.AncMode.Value.ON),
|
||||
))
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
val scope = TestScope(UnconfinedTestDispatcher())
|
||||
engine.start(scope)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
// First ANC mode = no debounce, applied immediately
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stable in-ear OFF infers AllowOffOption true after delay`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>()?.enabled shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `later non-OFF ANC update cancels pending AllowOffOption true inference`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>()?.enabled shouldBe true
|
||||
advanceTimeBy(500L)
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejected OFF command infers AllowOffOption false`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AllowOffOption(enabled = true))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF)) { sentCommands += it }
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(2100L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>()?.enabled shouldBe false
|
||||
sentCommands shouldBe listOf(
|
||||
AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF),
|
||||
AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user