mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
feat(device-settings): Merge one-pod mode settings into a single toggle
Combine reaction one-pod mode (autoplay/autopause) and firmware NC-with-one-AirPod into one setting. The merged toggle always sets the app-local flag and additionally syncs the firmware NC setting via AAP when connected. Also remove redundant @OptIn(ExperimentalCoroutinesApi::class) annotations from tests, already covered by the global compiler opt-in flag.
This commit is contained in:
@@ -150,7 +150,6 @@ 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) },
|
||||
@@ -187,7 +186,6 @@ fun DeviceSettingsScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
|
||||
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
||||
onNcWithOneAirPodChange: (Boolean) -> Unit = {},
|
||||
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
|
||||
onToneVolumeChange: (Int) -> Unit = {},
|
||||
onAdaptiveAudioNoiseChange: (Int) -> Unit = {},
|
||||
@@ -325,7 +323,8 @@ fun DeviceSettingsScreen(
|
||||
if (features.hasDualPods) {
|
||||
val onePodModeActive = reactions.autoPlay ||
|
||||
reactions.autoPause ||
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR) ||
|
||||
features.hasNcOneAirpod
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_onepod_mode_label),
|
||||
subtitle = stringResource(R.string.settings_onepod_mode_description),
|
||||
@@ -450,7 +449,6 @@ fun DeviceSettingsScreen(
|
||||
|
||||
// ── Noise Control ────────────────────────────
|
||||
val ancMode = device.ancMode
|
||||
val ncOneAirpod = device.ncWithOneAirPod
|
||||
val adaptiveNoise = device.adaptiveAudioNoise
|
||||
if (features.hasAncControl && ancMode != null) {
|
||||
val cycleMask = if (features.hasListeningModeCycle) {
|
||||
@@ -471,7 +469,6 @@ fun DeviceSettingsScreen(
|
||||
enabled = enabled,
|
||||
)
|
||||
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
|
||||
(features.hasNcOneAirpod && ncOneAirpod != null) ||
|
||||
(!isPro && features.hasListeningModeCycle)
|
||||
if (hasNoiseExtras) {
|
||||
HorizontalDivider(
|
||||
@@ -487,16 +484,6 @@ fun DeviceSettingsScreen(
|
||||
isAdaptiveMode = ancMode.current == AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
}
|
||||
if (features.hasNcOneAirpod && ncOneAirpod != null) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.Headphones,
|
||||
title = stringResource(R.string.device_settings_nc_one_airpod_label),
|
||||
subtitle = stringResource(R.string.device_settings_nc_one_airpod_description),
|
||||
checked = ncOneAirpod.enabled,
|
||||
onCheckedChange = onNcWithOneAirPodChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (!isPro && features.hasListeningModeCycle) {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.TwoTone.Loop,
|
||||
|
||||
@@ -206,8 +206,6 @@ 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) = send(AapCommand.SetToneVolume(level))
|
||||
@@ -299,7 +297,14 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
|
||||
fun setOnePodMode(enabled: Boolean) {
|
||||
log(TAG, INFO) { "setOnePodMode($enabled)" }
|
||||
launch { updateProfileNow { it.copy(onePodMode = 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoPlay(enabled: Boolean) = launch {
|
||||
|
||||
@@ -26,6 +26,7 @@ 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()" }
|
||||
@@ -34,6 +35,7 @@ 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" }
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,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">Treat a single in-ear pod as worn. Auto play/pause and "In ear" auto-connect react to either pod instead of requiring both.</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_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>
|
||||
|
||||
Reference in New Issue
Block a user