From 71b136dfceb55635af5a596bf47555019dfc91a6 Mon Sep 17 00:00:00 2001 From: QuantumBreakz Date: Sat, 18 Jul 2026 00:09:26 +0500 Subject: [PATCH] feat(android): add "both pods only" conversational awareness setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add conversationalAwarenessBothPodsOnly field to ServiceConfig (default false) - Seed 'conversational_awareness_both_pods_only' pref on first run (default false) - Load pref in initializeConfig() and handle live changes in onSharedPreferenceChanged() - Add conversationalAwarenessBothPodsOnlyEnabled to AppSettingsUiState - Add setConversationalAwarenessBothPodsOnlyEnabled() setter to AppSettingsViewModel - Add StyledToggle in AppSettingsScreen inside the Conversational Awareness list - Add string resources: conversational_awareness_both_pods_only + _description Default is off — no behavior change for existing users. Fixes #616 --- .../presentation/screens/AppSettingsScreen.kt | 8 ++++++++ .../viewmodel/AppSettingsViewModel.kt | 7 +++++++ .../librepods/services/AirPodsService.kt | 20 ++++++++++++++++--- android/app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt index 06436561d..0b4927562 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt @@ -230,6 +230,14 @@ fun AppSettingsScreen( enabled = state.isPremium ) + StyledToggle( + label = stringResource(R.string.conversational_awareness_both_pods_only), + description = stringResource(R.string.conversational_awareness_both_pods_only_description), + checked = state.conversationalAwarenessBothPodsOnlyEnabled, + onCheckedChange = viewModel::setConversationalAwarenessBothPodsOnlyEnabled, + enabled = state.isPremium, + ) + StyledToggle( label = stringResource(R.string.relative_conversational_awareness_volume), description = stringResource(R.string.relative_conversational_awareness_volume_description), diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt index 2c1716c33..606d7f0b2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt @@ -18,6 +18,7 @@ import kotlin.math.roundToInt data class AppSettingsUiState( val showPhoneBatteryInWidget: Boolean = false, val conversationalAwarenessPauseMusicEnabled: Boolean = false, + val conversationalAwarenessBothPodsOnlyEnabled: Boolean = false, val relativeConversationalAwarenessVolumeEnabled: Boolean = true, val disconnectWhenNotWearing: Boolean = false, val takeoverWhenDisconnected: Boolean = false, @@ -137,6 +138,7 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat currentState.copy( showPhoneBatteryInWidget = sharedPreferences.getBoolean("show_phone_battery_in_widget", false), conversationalAwarenessPauseMusicEnabled = sharedPreferences.getBoolean("conversational_awareness_pause_music", false), + conversationalAwarenessBothPodsOnlyEnabled = sharedPreferences.getBoolean("conversational_awareness_both_pods_only", false), relativeConversationalAwarenessVolumeEnabled = sharedPreferences.getBoolean("relative_conversational_awareness_volume", true), disconnectWhenNotWearing = sharedPreferences.getBoolean("disconnect_when_not_wearing", false), takeoverWhenDisconnected = sharedPreferences.getBoolean("takeover_when_disconnected", false), @@ -167,6 +169,11 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat _uiState.update { it.copy(conversationalAwarenessPauseMusicEnabled = enabled) } } + fun setConversationalAwarenessBothPodsOnlyEnabled(enabled: Boolean) { + sharedPreferences.edit { putBoolean("conversational_awareness_both_pods_only", enabled) } + _uiState.update { it.copy(conversationalAwarenessBothPodsOnlyEnabled = enabled) } + } + fun setRelativeConversationalAwarenessVolumeEnabled(enabled: Boolean) { sharedPreferences.edit { putBoolean("relative_conversational_awareness_volume", enabled) } _uiState.update { it.copy(relativeConversationalAwarenessVolumeEnabled = enabled) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 0cf08c11d..387ab6ba9 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -165,6 +165,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList var deviceName: String = "AirPods", var earDetectionEnabled: Boolean = true, var conversationalAwarenessPauseMusic: Boolean = false, + var conversationalAwarenessBothPodsOnly: Boolean = false, var showPhoneBatteryInWidget: Boolean = true, var relativeConversationalAwarenessVolume: Boolean = true, var headGestures: Boolean = true, @@ -440,6 +441,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList if (!contains("conversational_awareness_pause_music")) putBoolean( "conversational_awareness_pause_music", false ) + if (!contains("conversational_awareness_both_pods_only")) putBoolean( + "conversational_awareness_both_pods_only", false + ) if (!contains("personalized_volume")) putBoolean("personalized_volume", false) if (!contains("automatic_ear_detection")) putBoolean( "automatic_ear_detection", true @@ -912,10 +916,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList setPackage(packageName) }) + // Number of buds currently in the ear (0x00 == in ear). + val budsInEar = earDetectionNotification.status.count { it == 0x00.toByte() } + val blockedForSinglePod = config.conversationalAwarenessBothPodsOnly && budsInEar < 2 + if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) { - MediaController.startSpeaking() - } else if (conversationAwarenessNotification.status == 6.toByte() ||conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) { - MediaController.stopSpeaking() + if (!blockedForSinglePod) MediaController.startSpeaking() + } else if (conversationAwarenessNotification.status == 6.toByte() || conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) { + MediaController.stopSpeaking() // Never gate the "stop" side — volume must always be restored. } Log.d( @@ -1360,6 +1368,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList conversationalAwarenessPauseMusic = sharedPreferences.getBoolean( "conversational_awareness_pause_music", false ), + conversationalAwarenessBothPodsOnly = sharedPreferences.getBoolean( + "conversational_awareness_both_pods_only", false + ), showPhoneBatteryInWidget = sharedPreferences.getBoolean( "show_phone_battery_in_widget", true ), @@ -1473,6 +1484,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList "conversational_awareness_pause_music" -> config.conversationalAwarenessPauseMusic = preferences.getBoolean(key, false) + "conversational_awareness_both_pods_only" -> config.conversationalAwarenessBothPodsOnly = + preferences.getBoolean(key, false) + "show_phone_battery_in_widget" -> { config.showPhoneBatteryInWidget = preferences.getBoolean(key, true) widgetMobileBatteryEnabled = config.showPhoneBatteryInWidget diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 3356edc4a..050f09fd3 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -39,6 +39,8 @@ Reduces to a percentage of the current volume instead of the maximum volume. Pause Music When you start speaking, music will be paused. + Only when both AirPods are worn + Only lowers the volume when you speak if both AirPods are in your ears. EXAMPLE Add widget Control Noise Control Mode directly from your Home Screen.