feat(device-settings): Split ANC-with-one-pod into its own toggle

This commit is contained in:
darken
2026-04-17 13:02:14 +02:00
committed by Matthias Urhahn
parent 77ea31216f
commit 5ee094da3c
7 changed files with 68 additions and 326 deletions
@@ -158,6 +158,7 @@ fun DeviceSettingsScreenHost(
onNavigateUp = { vm.navUp() },
onAncModeChange = { vm.setAncMode(it) },
onConversationalAwarenessChange = { vm.setConversationalAwareness(it) },
onNcWithOneAirPodChange = { vm.setNcWithOneAirPod(it) },
onPersonalizedVolumeChange = { vm.setPersonalizedVolume(it) },
onToneVolumeChange = { vm.setToneVolume(it) },
onAdaptiveAudioNoiseChange = { vm.setAdaptiveAudioNoise(it) },
@@ -194,6 +195,7 @@ fun DeviceSettingsScreen(
onNavigateUp: () -> Unit,
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
onConversationalAwarenessChange: (Boolean) -> Unit = {},
onNcWithOneAirPodChange: (Boolean) -> Unit = {},
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
onToneVolumeChange: (Int) -> Unit = {},
onAdaptiveAudioNoiseChange: (Int) -> Unit = {},
@@ -369,10 +371,10 @@ fun DeviceSettingsScreen(
requiresUpgrade = !isPro,
)
if (features.hasDualPods) {
val onePodModeActive = reactions.autoPlay ||
val onePodModeActive = reactions.onePodMode ||
reactions.autoPlay ||
reactions.autoPause ||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR) ||
features.hasNcOneAirpod
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
SettingsBaseItem(
title = stringResource(R.string.settings_onepod_mode_label),
subtitle = stringResource(R.string.settings_onepod_mode_description),
@@ -547,14 +549,27 @@ fun DeviceSettingsScreen(
onModeSelected = onAncModeChange,
enabled = enabled,
)
val ncWithOneAirPod = device.ncWithOneAirPod
val showNcWithOneAirPod = features.hasNcOneAirpod && ncWithOneAirPod != null
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
features.hasListeningModeCycle
features.hasListeningModeCycle ||
showNcWithOneAirPod
if (hasNoiseExtras) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
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) {
SettingsPreferenceItem(
icon = Icons.TwoTone.Loop,
@@ -226,6 +226,8 @@ class DeviceSettingsViewModel @Inject constructor(
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 setToneVolume(level: Int) = sendProGated(AapCommand.SetToneVolume(level))
@@ -317,11 +319,6 @@ class DeviceSettingsViewModel @Inject constructor(
log(TAG, INFO) { "setOnePodMode($enabled)" }
launch {
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 stemConfigSender: StemConfigSender,
private val stemPressReaction: StemPressReaction,
private val ncOnePodSender: NcOnePodSender,
) {
fun start() {
log(TAG) { "start()" }
@@ -35,7 +34,6 @@ class AapLifecycleManager @Inject constructor(
aapKeyPersister.monitor(),
stemConfigSender.monitor(),
stemPressReaction.monitor(),
ncOnePodSender.monitor(),
)
.catch { e -> log(TAG, WARN) { "AAP lifecycle error: ${e.asLog()}" } }
.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")
}
}
+2 -2
View File
@@ -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_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_description">Show a popup when the device case is opened (experimental).</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_category_sound_label">Sound</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_personalized_volume_label">Personalized Volume</string>
<string name="device_settings_personalized_volume_description">Adjust media volume based on environment</string>