mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
feat(device-settings): Split ANC-with-one-pod into its own toggle
This commit is contained in:
@@ -158,6 +158,7 @@ fun DeviceSettingsScreenHost(
|
|||||||
onNavigateUp = { vm.navUp() },
|
onNavigateUp = { vm.navUp() },
|
||||||
onAncModeChange = { vm.setAncMode(it) },
|
onAncModeChange = { vm.setAncMode(it) },
|
||||||
onConversationalAwarenessChange = { vm.setConversationalAwareness(it) },
|
onConversationalAwarenessChange = { vm.setConversationalAwareness(it) },
|
||||||
|
onNcWithOneAirPodChange = { vm.setNcWithOneAirPod(it) },
|
||||||
onPersonalizedVolumeChange = { vm.setPersonalizedVolume(it) },
|
onPersonalizedVolumeChange = { vm.setPersonalizedVolume(it) },
|
||||||
onToneVolumeChange = { vm.setToneVolume(it) },
|
onToneVolumeChange = { vm.setToneVolume(it) },
|
||||||
onAdaptiveAudioNoiseChange = { vm.setAdaptiveAudioNoise(it) },
|
onAdaptiveAudioNoiseChange = { vm.setAdaptiveAudioNoise(it) },
|
||||||
@@ -194,6 +195,7 @@ fun DeviceSettingsScreen(
|
|||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
|
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
|
||||||
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
||||||
|
onNcWithOneAirPodChange: (Boolean) -> Unit = {},
|
||||||
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
|
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
|
||||||
onToneVolumeChange: (Int) -> Unit = {},
|
onToneVolumeChange: (Int) -> Unit = {},
|
||||||
onAdaptiveAudioNoiseChange: (Int) -> Unit = {},
|
onAdaptiveAudioNoiseChange: (Int) -> Unit = {},
|
||||||
@@ -369,10 +371,10 @@ fun DeviceSettingsScreen(
|
|||||||
requiresUpgrade = !isPro,
|
requiresUpgrade = !isPro,
|
||||||
)
|
)
|
||||||
if (features.hasDualPods) {
|
if (features.hasDualPods) {
|
||||||
val onePodModeActive = reactions.autoPlay ||
|
val onePodModeActive = reactions.onePodMode ||
|
||||||
|
reactions.autoPlay ||
|
||||||
reactions.autoPause ||
|
reactions.autoPause ||
|
||||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR) ||
|
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||||
features.hasNcOneAirpod
|
|
||||||
SettingsBaseItem(
|
SettingsBaseItem(
|
||||||
title = stringResource(R.string.settings_onepod_mode_label),
|
title = stringResource(R.string.settings_onepod_mode_label),
|
||||||
subtitle = stringResource(R.string.settings_onepod_mode_description),
|
subtitle = stringResource(R.string.settings_onepod_mode_description),
|
||||||
@@ -547,14 +549,27 @@ fun DeviceSettingsScreen(
|
|||||||
onModeSelected = onAncModeChange,
|
onModeSelected = onAncModeChange,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
|
val ncWithOneAirPod = device.ncWithOneAirPod
|
||||||
|
val showNcWithOneAirPod = features.hasNcOneAirpod && ncWithOneAirPod != null
|
||||||
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
|
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
|
||||||
features.hasListeningModeCycle
|
features.hasListeningModeCycle ||
|
||||||
|
showNcWithOneAirPod
|
||||||
if (hasNoiseExtras) {
|
if (hasNoiseExtras) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showNcWithOneAirPod) {
|
||||||
|
SettingsSwitchItem(
|
||||||
|
icon = Icons.TwoTone.LooksOne,
|
||||||
|
title = stringResource(R.string.device_settings_nc_one_airpod_label),
|
||||||
|
subtitle = stringResource(R.string.device_settings_nc_one_airpod_description),
|
||||||
|
checked = ncWithOneAirPod!!.enabled,
|
||||||
|
onCheckedChange = onNcWithOneAirPodChange,
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (features.hasListeningModeCycle && cycleMask != null && cycleSubtitle != null) {
|
if (features.hasListeningModeCycle && cycleMask != null && cycleSubtitle != null) {
|
||||||
SettingsPreferenceItem(
|
SettingsPreferenceItem(
|
||||||
icon = Icons.TwoTone.Loop,
|
icon = Icons.TwoTone.Loop,
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun setConversationalAwareness(enabled: Boolean) = send(AapCommand.SetConversationalAwareness(enabled))
|
fun setConversationalAwareness(enabled: Boolean) = send(AapCommand.SetConversationalAwareness(enabled))
|
||||||
|
|
||||||
|
fun setNcWithOneAirPod(enabled: Boolean) = send(AapCommand.SetNcWithOneAirPod(enabled))
|
||||||
|
|
||||||
fun setPersonalizedVolume(enabled: Boolean) = send(AapCommand.SetPersonalizedVolume(enabled))
|
fun setPersonalizedVolume(enabled: Boolean) = send(AapCommand.SetPersonalizedVolume(enabled))
|
||||||
|
|
||||||
fun setToneVolume(level: Int) = sendProGated(AapCommand.SetToneVolume(level))
|
fun setToneVolume(level: Int) = sendProGated(AapCommand.SetToneVolume(level))
|
||||||
@@ -317,11 +319,6 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
log(TAG, INFO) { "setOnePodMode($enabled)" }
|
log(TAG, INFO) { "setOnePodMode($enabled)" }
|
||||||
launch {
|
launch {
|
||||||
updateProfileNow { it.copy(onePodMode = enabled) }
|
updateProfileNow { it.copy(onePodMode = enabled) }
|
||||||
// Opportunistic immediate sync — NcOnePodSender handles deferred/reconnect
|
|
||||||
val device = deviceMonitor.getDeviceForProfile(targetProfileId.value ?: return@launch)
|
|
||||||
if (device != null && device.isAapConnected && device.model.features.hasNcOneAirpod) {
|
|
||||||
sendInternal(AapCommand.SetNcWithOneAirPod(enabled))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class AapLifecycleManager @Inject constructor(
|
|||||||
private val aapKeyPersister: AapKeyPersister,
|
private val aapKeyPersister: AapKeyPersister,
|
||||||
private val stemConfigSender: StemConfigSender,
|
private val stemConfigSender: StemConfigSender,
|
||||||
private val stemPressReaction: StemPressReaction,
|
private val stemPressReaction: StemPressReaction,
|
||||||
private val ncOnePodSender: NcOnePodSender,
|
|
||||||
) {
|
) {
|
||||||
fun start() {
|
fun start() {
|
||||||
log(TAG) { "start()" }
|
log(TAG) { "start()" }
|
||||||
@@ -35,7 +34,6 @@ class AapLifecycleManager @Inject constructor(
|
|||||||
aapKeyPersister.monitor(),
|
aapKeyPersister.monitor(),
|
||||||
stemConfigSender.monitor(),
|
stemConfigSender.monitor(),
|
||||||
stemPressReaction.monitor(),
|
stemPressReaction.monitor(),
|
||||||
ncOnePodSender.monitor(),
|
|
||||||
)
|
)
|
||||||
.catch { e -> log(TAG, WARN) { "AAP lifecycle error: ${e.asLog()}" } }
|
.catch { e -> log(TAG, WARN) { "AAP lifecycle error: ${e.asLog()}" } }
|
||||||
.setupCommonEventHandlers(TAG) { "aapActive" }
|
.setupCommonEventHandlers(TAG) { "aapActive" }
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core.aap
|
|
||||||
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|
||||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
|
||||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
|
||||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class NcOnePodSender @Inject constructor(
|
|
||||||
private val aapManager: AapConnectionManager,
|
|
||||||
private val profilesRepo: DeviceProfilesRepo,
|
|
||||||
) {
|
|
||||||
fun monitor(): Flow<Unit> = combine(
|
|
||||||
aapManager.allStates,
|
|
||||||
profilesRepo.profiles,
|
|
||||||
) { states, profiles ->
|
|
||||||
val appleProfiles = profiles.filterIsInstance<AppleDeviceProfile>()
|
|
||||||
states.entries
|
|
||||||
.filter { (_, s) -> s.connectionState == AapPodState.ConnectionState.READY }
|
|
||||||
.mapNotNull { (address, _) ->
|
|
||||||
val profile = appleProfiles.firstOrNull { it.address == address }
|
|
||||||
if (profile != null && profile.model.features.hasNcOneAirpod) {
|
|
||||||
address to profile.onePodMode
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { commands ->
|
|
||||||
for ((address, enabled) in commands) {
|
|
||||||
try {
|
|
||||||
aapManager.sendCommand(address, AapCommand.SetNcWithOneAirPod(enabled))
|
|
||||||
log(TAG) { "Sent SetNcWithOneAirPod($enabled) to $address" }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, WARN) { "SetNcWithOneAirPod send failed for $address: $e" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { }
|
|
||||||
.setupCommonEventHandlers(TAG) { "ncOnePod" }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = logTag("Monitor", "NcOnePodSender")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<string name="settings_compat_offloaded_batching_disabled_summary">Don\'t let the system group collected BLE data before forwarding it to us.</string>
|
<string name="settings_compat_offloaded_batching_disabled_summary">Don\'t let the system group collected BLE data before forwarding it to us.</string>
|
||||||
|
|
||||||
<string name="settings_onepod_mode_label">One pod mode</string>
|
<string name="settings_onepod_mode_label">One pod mode</string>
|
||||||
<string name="settings_onepod_mode_description">Auto play/pause, auto-connect, and on supported models noise cancellation, react to a single pod instead of requiring both.</string>
|
<string name="settings_onepod_mode_description">Auto play/pause and auto-connect react to a single pod instead of requiring both.</string>
|
||||||
<string name="settings_popup_caseopen_label">Show case popup</string>
|
<string name="settings_popup_caseopen_label">Show case popup</string>
|
||||||
<string name="settings_popup_caseopen_description">Show a popup when the device case is opened (experimental).</string>
|
<string name="settings_popup_caseopen_description">Show a popup when the device case is opened (experimental).</string>
|
||||||
<string name="settings_popup_connected_label">Show connection popup</string>
|
<string name="settings_popup_connected_label">Show connection popup</string>
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
<string name="device_settings_aap_unavailable_description">Advanced AirPods settings require a Bluetooth feature that is not available on this phone. This may be resolved by a future Android update from your device manufacturer.</string>
|
<string name="device_settings_aap_unavailable_description">Advanced AirPods settings require a Bluetooth feature that is not available on this phone. This may be resolved by a future Android update from your device manufacturer.</string>
|
||||||
<string name="device_settings_category_sound_label">Sound</string>
|
<string name="device_settings_category_sound_label">Sound</string>
|
||||||
<string name="device_settings_category_controls_label">Controls</string>
|
<string name="device_settings_category_controls_label">Controls</string>
|
||||||
<string name="device_settings_nc_one_airpod_label">Noise Cancellation with One AirPod</string>
|
<string name="device_settings_nc_one_airpod_label">ANC with one pod</string>
|
||||||
<string name="device_settings_nc_one_airpod_description">Keep noise cancellation active with only one AirPod</string>
|
<string name="device_settings_nc_one_airpod_description">Keep noise cancellation active with only one AirPod</string>
|
||||||
<string name="device_settings_personalized_volume_label">Personalized Volume</string>
|
<string name="device_settings_personalized_volume_label">Personalized Volume</string>
|
||||||
<string name="device_settings_personalized_volume_description">Adjust media volume based on environment</string>
|
<string name="device_settings_personalized_volume_description">Adjust media volume based on environment</string>
|
||||||
|
|||||||
+45
@@ -346,4 +346,49 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
|||||||
|
|
||||||
vm.state.first().hasCustomLongPressStemAction shouldBe true
|
vm.state.first().hasCustomLongPressStemAction shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setOnePodMode updates profile and does not send SetNcWithOneAirPod`() = runVmTest {
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.initialize(testAddress)
|
||||||
|
vm.state.first()
|
||||||
|
|
||||||
|
vm.setOnePodMode(true)
|
||||||
|
|
||||||
|
coVerify { profilesRepo.updateAppleProfile(testAddress, any()) }
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
aapManager.sendCommand(any(), any<AapCommand.SetNcWithOneAirPod>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setNcWithOneAirPod sends command and does not touch profile`() = runVmTest {
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.initialize(testAddress)
|
||||||
|
vm.state.first()
|
||||||
|
|
||||||
|
vm.setNcWithOneAirPod(true)
|
||||||
|
|
||||||
|
coVerify { aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true)) }
|
||||||
|
coVerify(exactly = 0) { profilesRepo.updateAppleProfile(any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setNcWithOneAirPod failure emits SendFailed event`() = runVmTest {
|
||||||
|
val failure = IllegalStateException("socket closed")
|
||||||
|
coEvery {
|
||||||
|
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
||||||
|
} throws failure
|
||||||
|
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.initialize(testAddress)
|
||||||
|
vm.state.first()
|
||||||
|
|
||||||
|
vm.setNcWithOneAirPod(true)
|
||||||
|
|
||||||
|
val event = vm.events.first()
|
||||||
|
val sendFailed = event.shouldBeInstanceOf<DeviceSettingsViewModel.Event.SendFailed>()
|
||||||
|
sendFailed.command shouldBe AapCommand.SetNcWithOneAirPod(true)
|
||||||
|
sendFailed.message shouldBe "socket closed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core.aap
|
|
||||||
|
|
||||||
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.pods.core.apple.aap.protocol.AapCommand
|
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.coVerify
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import testhelpers.BaseTest
|
|
||||||
|
|
||||||
class NcOnePodSenderTest : BaseTest() {
|
|
||||||
|
|
||||||
private val testDispatcher = UnconfinedTestDispatcher()
|
|
||||||
|
|
||||||
private lateinit var aapManager: AapConnectionManager
|
|
||||||
private lateinit var profilesRepo: DeviceProfilesRepo
|
|
||||||
|
|
||||||
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
|
|
||||||
private lateinit var allStatesFlow: MutableStateFlow<Map<String, AapPodState>>
|
|
||||||
|
|
||||||
private val testAddress = "AA:BB:CC:DD:EE:FF"
|
|
||||||
private val testAddress2 = "11:22:33:44:55:66"
|
|
||||||
|
|
||||||
private val proProfile = AppleDeviceProfile(
|
|
||||||
label = "AirPods Pro 3",
|
|
||||||
model = PodModel.AIRPODS_PRO3,
|
|
||||||
address = testAddress,
|
|
||||||
onePodMode = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val nonNcProfile = AppleDeviceProfile(
|
|
||||||
label = "AirPods Gen 3",
|
|
||||||
model = PodModel.UNKNOWN,
|
|
||||||
address = testAddress2,
|
|
||||||
onePodMode = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
profilesFlow = MutableStateFlow(emptyList())
|
|
||||||
allStatesFlow = MutableStateFlow(emptyMap())
|
|
||||||
|
|
||||||
aapManager = mockk(relaxed = true) {
|
|
||||||
every { allStates } returns allStatesFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
profilesRepo = mockk(relaxUnitFun = true) {
|
|
||||||
every { profiles } returns profilesFlow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSender() = NcOnePodSender(
|
|
||||||
aapManager = aapManager,
|
|
||||||
profilesRepo = profilesRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `sends enabled when READY and onePodMode is true`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = true))
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `sends disabled when READY and onePodMode is false`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = false))
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `does not send when device does not support NC one airpod`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(nonNcProfile)
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress2 to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { aapManager.sendCommand(any(), any<AapCommand.SetNcWithOneAirPod>()) }
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `does not send when connection is not READY`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = true))
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.CONNECTING)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { aapManager.sendCommand(any(), any<AapCommand.SetNcWithOneAirPod>()) }
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `sends again when onePodMode toggles`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enable
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = true))
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
// Disable
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = false))
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(ordering = io.mockk.Ordering.ORDERED) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `does not resend for unrelated state changes`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = true))
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
// Unrelated AAP state change (e.g. battery update) — same READY, same profile
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(
|
|
||||||
connectionState = AapPodState.ConnectionState.READY,
|
|
||||||
lastMessageAt = java.time.Instant.now(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
// Should only have sent once — distinctUntilChanged filters the second emission
|
|
||||||
coVerify(exactly = 1) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `handles send failure without crashing the flow`() = runTest(testDispatcher) {
|
|
||||||
coEvery {
|
|
||||||
aapManager.sendCommand(any(), any<AapCommand.SetNcWithOneAirPod>())
|
|
||||||
} throws RuntimeException("Connection lost")
|
|
||||||
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(proProfile.copy(onePodMode = true))
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
// Flow should survive the exception
|
|
||||||
coVerify(exactly = 1) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `does not send to unmatched addresses`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
// Profile for testAddress, but AAP connection on a different address
|
|
||||||
profilesFlow.value = listOf(proProfile)
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
"XX:XX:XX:XX:XX:XX" to AapPodState(connectionState = AapPodState.ConnectionState.READY)
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { aapManager.sendCommand(any(), any<AapCommand.SetNcWithOneAirPod>()) }
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `only sends to NC-capable device when multiple devices connected`() = runTest(testDispatcher) {
|
|
||||||
val sender = createSender()
|
|
||||||
val job = launch { sender.monitor().toList() }
|
|
||||||
|
|
||||||
profilesFlow.value = listOf(
|
|
||||||
proProfile.copy(onePodMode = true),
|
|
||||||
nonNcProfile,
|
|
||||||
)
|
|
||||||
allStatesFlow.value = mapOf(
|
|
||||||
testAddress to AapPodState(connectionState = AapPodState.ConnectionState.READY),
|
|
||||||
testAddress2 to AapPodState(connectionState = AapPodState.ConnectionState.READY),
|
|
||||||
)
|
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
|
||||||
aapManager.sendCommand(testAddress, AapCommand.SetNcWithOneAirPod(true))
|
|
||||||
}
|
|
||||||
coVerify(exactly = 0) {
|
|
||||||
aapManager.sendCommand(testAddress2, any<AapCommand.SetNcWithOneAirPod>())
|
|
||||||
}
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user